feat: restore wasm support and vscode extension

This commit is contained in:
tamasfe 2022-06-12 16:01:16 +02:00
parent 05ead961d8
commit 78856eb767
No known key found for this signature in database
GPG key ID: 2373047D27CA4E47
74 changed files with 8096 additions and 1171 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ Cargo.lock
target
.vscode/settings.json
.cargo/config.toml
yarn-error.log

View file

@ -1,6 +1,6 @@
[workspace]
exclude = ["util/test-gen", "util/schema-index"]
members = ["crates/*"]
exclude = ["util/test-gen", "util/schema-index", "crates/taplo-wasm"]
[profile.release]
codegen-units = 1

View file

@ -11,24 +11,19 @@ categories = ["development-tools", "command-line-utilities"]
keywords = ["toml", "linter", "formatter"]
[features]
default = ["lsp"]
lsp = ["taplo-lsp"]
default = []
lsp = ["taplo-lsp", "async-ctrlc"]
toml-test = []
[dependencies]
anyhow = { version = "1", features = ["backtrace"] }
async-ctrlc = { version = "1.2.0", features = ["stream"] }
atty = "0.2.14"
async-ctrlc = { version = "1.2.0", features = ["stream"], optional = true }
clap = { version = "3.0.0", features = ["derive", "cargo"] }
codespan-reporting = "0.11.1"
futures = "0.3"
glob = "0.3"
hex = "0.4"
itertools = "0.10.3"
lsp-async-stub = { version = "0.5.0", path = "../lsp-async-stub", features = [
"tokio-tcp",
"tokio-stdio",
] }
once_cell = "1.4"
regex = "1.4"
reqwest = { version = "0.11.4", features = ["json"] }
@ -44,9 +39,24 @@ tracing = "0.1.29"
tracing-subscriber = { version = "0.3.7", features = ["env-filter"] }
url = "2.2.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
atty = "0.2.14"
tokio = { version = "1.19.2", features = [
"sync",
"fs",
"time",
"io-std",
"rt-multi-thread",
"parking_lot",
] }
lsp-async-stub = { version = "0.5.0", path = "../lsp-async-stub", features = [
"tokio-tcp",
"tokio-stdio",
] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { version = "1.19.2", features = ["sync", "parking_lot", "io-util"] }
lsp-async-stub = { version = "0.5.0", path = "../lsp-async-stub" }
[[bin]]
name = "taplo"

View file

@ -1,13 +1,25 @@
use clap::StructOpt;
use std::process::exit;
use taplo_cli::{args::TaploArgs, log::setup_stderr_logging, Taplo};
use taplo_common::environment::native::NativeEnvironment;
use taplo_cli::{
args::{Colors, TaploArgs},
Taplo,
};
use taplo_common::{environment::native::NativeEnvironment, log::setup_stderr_logging};
use tracing::Instrument;
#[tokio::main]
async fn main() {
let cli = TaploArgs::parse();
setup_stderr_logging(NativeEnvironment::new(), &cli);
setup_stderr_logging(
NativeEnvironment::new(),
cli.log_spans,
cli.verbose,
match cli.colors {
Colors::Auto => None,
Colors::Always => Some(true),
Colors::Never => Some(false),
},
);
match Taplo::new(NativeEnvironment::new())
.execute(cli)

View file

@ -16,7 +16,7 @@ impl<E: Environment> Taplo<E> {
}
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
async fn format_stdin(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
let mut source = String::new();
self.env.stdin().read_to_string(&mut source).await?;
@ -63,7 +63,7 @@ impl<E: Environment> Taplo<E> {
Ok(())
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
async fn format_files(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
if cmd.stdin_filepath.is_some() {
tracing::warn!("using `--stdin-filepath` has no effect unless input comes from stdin")

View file

@ -60,14 +60,14 @@ impl<E: Environment> Taplo<E> {
}
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
async fn lint_stdin(&self, _cmd: LintCommand) -> Result<(), anyhow::Error> {
let mut source = String::new();
self.env.stdin().read_to_string(&mut source).await?;
self.lint_source("-", &source).await
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
async fn lint_files(&mut self, cmd: LintCommand) -> Result<(), anyhow::Error> {
let config = self.load_config(&cmd.general).await?;
@ -93,7 +93,7 @@ impl<E: Environment> Taplo<E> {
}
async fn lint_file(&self, file: &Path) -> Result<(), anyhow::Error> {
let source = self.env.read_file(&file).await?;
let source = self.env.read_file(file).await?;
let source = String::from_utf8(source)?;
self.lint_source(&*file.to_string_lossy(), &source).await
}

View file

@ -1,17 +1,14 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use anyhow::{anyhow, Context};
use args::GeneralArgs;
use itertools::Itertools;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use taplo_common::{config::Config, environment::Environment, schema::Schemas};
pub mod args;
pub mod commands;
pub mod log;
pub mod printing;
pub struct Taplo<E: Environment> {
@ -23,11 +20,15 @@ pub struct Taplo<E: Environment> {
impl<E: Environment> Taplo<E> {
pub fn new(env: E) -> Self {
#[cfg(not(target_arch = "wasm32"))]
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.timeout(std::time::Duration::from_secs(5))
.build()
.unwrap();
#[cfg(target_arch = "wasm32")]
let http = reqwest::Client::default();
Self {
schemas: Schemas::new(env.clone(), http),
colors: env.atty_stderr(),
@ -36,7 +37,7 @@ impl<E: Environment> Taplo<E> {
}
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
async fn load_config(&mut self, general: &GeneralArgs) -> Result<Arc<Config>, anyhow::Error> {
if let Some(c) = self.config.clone() {
return Ok(c);
@ -53,7 +54,7 @@ impl<E: Environment> Taplo<E> {
let mut config = Config::default();
if let Some(c) = config_path {
tracing::info!(path = ?c, "found configuration file");
match self.env.read_file(c).await {
match self.env.read_file(&c).await {
Ok(cfg) => match toml::from_slice(&cfg) {
Ok(c) => config = c,
Err(error) => {
@ -83,7 +84,7 @@ impl<E: Environment> Taplo<E> {
Ok(c)
}
#[tracing::instrument(level = "debug", skip_all, fields(?cwd))]
#[tracing::instrument(skip_all, fields(?cwd))]
async fn collect_files(
&self,
cwd: &Path,

View file

@ -1,74 +0,0 @@
use crate::args::{Colors, TaploArgs};
use std::io;
use taplo_common::environment::Environment;
use tracing_subscriber::{
fmt::format::FmtSpan, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt,
EnvFilter,
};
pub fn setup_stderr_logging(e: impl Environment, taplo: &TaploArgs) {
let span_events = if taplo.log_spans {
FmtSpan::NEW | FmtSpan::CLOSE
} else {
FmtSpan::NONE
};
let registry = tracing_subscriber::registry();
let env_filter = match e.env_var("RUST_LOG") {
Some(log) => EnvFilter::new(log),
None => EnvFilter::default().add_directive(tracing::Level::INFO.into()),
};
if taplo.verbose {
registry
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(match taplo.colors {
Colors::Auto => e.atty_stderr(),
Colors::Always => true,
Colors::Never => false,
})
.with_span_events(span_events)
.event_format(tracing_subscriber::fmt::format().pretty().with_ansi(
match taplo.colors {
Colors::Auto => e.atty_stderr(),
Colors::Always => true,
Colors::Never => false,
},
))
.with_writer(io::stderr),
)
.init();
} else {
registry
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(match taplo.colors {
Colors::Auto => e.atty_stderr(),
Colors::Always => true,
Colors::Never => false,
})
.event_format(
tracing_subscriber::fmt::format()
.compact()
.with_source_location(false)
.with_target(false)
.without_time()
.with_ansi(match taplo.colors {
Colors::Auto => e.atty_stderr(),
Colors::Always => true,
Colors::Never => false,
}),
)
.without_time()
.with_file(false)
.with_line_number(false)
.with_span_events(span_events)
.with_writer(io::stderr),
)
.init();
}
}

View file

@ -35,14 +35,20 @@ tap = "1.0.1"
taplo = { version = "0.8.0", path = "../taplo", features = ["schema"] }
thiserror = "1.0.30"
time = { version = "0.3.7", features = ["serde"] }
tokio = { version = "1.16.1", features = [
tracing = "0.1.29"
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
url = { version = "2.2.2", features = ["serde"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.19.2", features = [
"sync",
"fs",
"time",
"io-std",
"parking_lot",
] }
tracing = "0.1.29"
url = { version = "2.2.2", features = ["serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { version = "1.19.2", features = ["sync", "parking_lot", "io-util"] }
[features]

View file

@ -339,10 +339,7 @@ impl Rule {
pub fn is_included(&self, path: &Path) -> bool {
match &self.file_rule {
Some(r) => r.is_match(path),
None => {
tracing::warn!("no file matches were set up");
false
}
None => true,
}
}
}

View file

@ -1,10 +1,11 @@
use async_trait::async_trait;
use futures::{future::LocalBoxFuture, Future};
use futures::Future;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncWrite};
use url::Url;
#[cfg(not(target_family = "wasm"))]
pub mod native;
/// An environment in which the operations with Taplo are executed.
@ -18,16 +19,11 @@ pub trait Environment: Clone + Send + Sync + 'static {
fn now(&self) -> OffsetDateTime;
fn spawn<F>(&self, fut: F) -> LocalBoxFuture<'static, F::Output>
fn spawn<F>(&self, fut: F)
where
F: Future + Send + 'static,
F::Output: Send;
fn spawn_blocking<F, R>(&self, cb: F) -> LocalBoxFuture<'static, R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static;
fn spawn_local<F>(&self, fut: F)
where
F: Future + 'static;
@ -41,10 +37,7 @@ pub trait Environment: Clone + Send + Sync + 'static {
fn glob_files(&self, glob: &str) -> Result<Vec<PathBuf>, anyhow::Error>;
async fn read_file(
&self,
path: impl AsRef<Path> + 'async_trait,
) -> Result<Vec<u8>, anyhow::Error>;
async fn read_file(&self, path: &Path) -> Result<Vec<u8>, anyhow::Error>;
async fn write_file(&self, path: &Path, bytes: &[u8]) -> Result<(), anyhow::Error>;
@ -55,5 +48,5 @@ pub trait Environment: Clone + Send + Sync + 'static {
/// Absolute current working dir.
fn cwd(&self) -> Option<PathBuf>;
async fn find_config_file(&self, from: impl AsRef<Path> + 'async_trait) -> Option<PathBuf>;
async fn find_config_file(&self, from: &Path) -> Option<PathBuf>;
}

View file

@ -1,8 +1,9 @@
use std::path::Path;
use crate::config::CONFIG_FILE_NAMES;
use super::Environment;
use async_trait::async_trait;
use futures::{future::LocalBoxFuture, FutureExt};
use time::OffsetDateTime;
#[derive(Clone)]
@ -35,22 +36,12 @@ impl Environment for NativeEnvironment {
OffsetDateTime::now_utc()
}
fn spawn<F>(&self, fut: F) -> LocalBoxFuture<'static, F::Output>
fn spawn<F>(&self, fut: F)
where
F: futures::Future + Send + 'static,
F::Output: Send,
{
let handle = self.handle.spawn(fut);
{ async move { handle.await.unwrap() } }.boxed_local()
}
fn spawn_blocking<F, R>(&self, cb: F) -> LocalBoxFuture<'static, R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let handle = self.handle.spawn_blocking(cb);
async move { handle.await.unwrap() }.boxed_local()
self.handle.spawn(fut);
}
fn spawn_local<F>(&self, fut: F)
@ -91,10 +82,7 @@ impl Environment for NativeEnvironment {
Ok(paths.filter_map(Result::ok).collect())
}
async fn read_file(
&self,
path: impl AsRef<std::path::Path> + 'async_trait,
) -> Result<Vec<u8>, anyhow::Error> {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>, anyhow::Error> {
Ok(tokio::fs::read(path).await?)
}
@ -114,11 +102,8 @@ impl Environment for NativeEnvironment {
std::env::current_dir().ok()
}
async fn find_config_file(
&self,
from: impl AsRef<std::path::Path> + 'async_trait,
) -> Option<std::path::PathBuf> {
let mut p = from.as_ref();
async fn find_config_file(&self, from: &Path) -> Option<std::path::PathBuf> {
let mut p = from;
loop {
if let Ok(mut dir) = tokio::fs::read_dir(p).await {

View file

@ -16,6 +16,7 @@ pub mod convert;
pub mod environment;
pub mod schema;
pub mod util;
pub mod log;
pub type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, ahash::RandomState>;

View file

@ -0,0 +1,118 @@
use std::io::{self, Write};
use tokio::io::{AsyncWrite, AsyncWriteExt};
use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, util::SubscriberInitExt, EnvFilter};
use crate::environment::Environment;
struct BlockingWrite<W: AsyncWrite>(W);
impl<W: AsyncWrite + Unpin> Write for BlockingWrite<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
#[cfg(not(target_arch = "wasm32"))]
{
futures::executor::block_on(self.0.write(buf))
}
// On WASM we cannot do blocking writes without blocking
// the event loop, so we simply do not wait.
#[cfg(target_arch = "wasm32")]
{
use futures::FutureExt;
let _ = self.0.write_all(buf).boxed_local().poll_unpin(
&mut futures::task::Context::from_waker(&futures::task::noop_waker()),
);
Ok(buf.len())
}
}
fn flush(&mut self) -> io::Result<()> {
#[cfg(not(target_arch = "wasm32"))]
{
futures::executor::block_on(self.0.flush())
}
// On WASM we cannot do blocking writes without blocking
// the event loop, so we simply do not wait.
#[cfg(target_arch = "wasm32")]
{
use futures::FutureExt;
let _ =
self.0
.flush()
.boxed_local()
.poll_unpin(&mut futures::task::Context::from_waker(
&futures::task::noop_waker(),
));
Ok(())
}
}
}
pub fn setup_stderr_logging(e: impl Environment, spans: bool, verbose: bool, colors: Option<bool>) {
let span_events = if spans {
FmtSpan::NEW | FmtSpan::CLOSE
} else {
FmtSpan::NONE
};
let registry = tracing_subscriber::registry();
let env_filter = match e.env_var("RUST_LOG") {
Some(log) => EnvFilter::new(log),
None => EnvFilter::default().add_directive(tracing::Level::INFO.into()),
};
if verbose {
registry
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(match colors {
None => e.atty_stderr(),
Some(v) => v,
})
.with_span_events(span_events)
.event_format(tracing_subscriber::fmt::format().pretty().with_ansi(
match colors {
None => e.atty_stderr(),
Some(v) => v,
},
))
.with_writer(move || BlockingWrite(e.stderr())),
)
.try_init()
.ok();
} else {
registry
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(match colors {
None => e.atty_stderr(),
Some(v) => v,
})
.event_format(
tracing_subscriber::fmt::format()
.compact()
.with_source_location(false)
.with_target(false)
.without_time()
.with_ansi(match colors {
None => e.atty_stderr(),
Some(v) => v,
}),
)
.without_time()
.with_file(false)
.with_line_number(false)
.with_span_events(span_events)
.with_writer(move || BlockingWrite(e.stderr())),
)
.try_init()
.ok();
}
}

View file

@ -328,7 +328,8 @@ impl<E: Environment> SchemaAssociations<E> {
.read_file(
self.env
.to_file_path(index_url)
.ok_or_else(|| anyhow!("invalid file path"))?,
.ok_or_else(|| anyhow!("invalid file path"))?
.as_ref(),
)
.await?,
)?),

View file

@ -59,7 +59,7 @@ impl<E: Environment> Schemas<E> {
}
impl<E: Environment> Schemas<E> {
#[tracing::instrument(level = "debug", skip_all, fields(%schema_url))]
#[tracing::instrument(skip_all, fields(%schema_url))]
pub async fn validate_root(
&self,
schema_url: &Url,
@ -73,7 +73,7 @@ impl<E: Environment> Schemas<E> {
.collect::<Result<Vec<_>, _>>()
}
#[tracing::instrument(level = "debug", skip_all, fields(%schema_url))]
#[tracing::instrument(skip_all, fields(%schema_url))]
pub async fn validate(
&self,
schema_url: &Url,
@ -161,7 +161,7 @@ impl<E: Environment> Schemas<E> {
drop(self.cache.store(schema_url.clone(), schema).await);
}
#[tracing::instrument(level = "debug", skip_all, fields(%schema_url))]
#[tracing::instrument(skip_all, fields(%schema_url))]
pub async fn load_schema(&self, schema_url: &Url) -> Result<Arc<Value>, anyhow::Error> {
if let Ok(s) = self.cache.load(schema_url, false).await {
tracing::debug!(%schema_url, "schema was found in cache");
@ -255,7 +255,8 @@ impl<E: Environment> Schemas<E> {
.read_file(
self.env
.to_file_path(schema_url)
.ok_or_else(|| anyhow!("invalid file path"))?,
.ok_or_else(|| anyhow!("invalid file path"))?
.as_ref(),
)
.await?,
)?),
@ -265,7 +266,7 @@ impl<E: Environment> Schemas<E> {
}
impl<E: Environment> Schemas<E> {
#[tracing::instrument(level = "debug", skip_all, fields(%schema_url, %path))]
#[tracing::instrument(skip_all, fields(%schema_url, %path))]
pub async fn schemas_at_path(
&self,
schema_url: &Url,
@ -292,7 +293,7 @@ impl<E: Environment> Schemas<E> {
Ok(schemas)
}
#[tracing::instrument(level = "debug", skip_all, fields(%path))]
#[tracing::instrument(skip_all, fields(%path))]
#[async_recursion(?Send)]
#[must_use]
async fn collect_schemas(
@ -431,7 +432,7 @@ impl<E: Environment> Schemas<E> {
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, fields(%schema_url, %path))]
#[tracing::instrument(skip_all, fields(%schema_url, %path))]
pub async fn possible_schemas_from(
&self,
schema_url: &Url,

View file

@ -37,4 +37,3 @@ taplo-common = { version = "0.1.2", path = "../taplo-common" }
time = { version = "0.3", features = ["formatting", "parsing"] }
toml = "0.5"
tracing = "0.1.29"

View file

@ -8,7 +8,7 @@ use lsp_types::{
use taplo::dom::{KeyOrIndex, Node};
use taplo_common::environment::Environment;
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn publish_diagnostics<E: Environment>(
mut context: Context<World<E>>,
ws_url: Url,
@ -102,7 +102,7 @@ pub(crate) async fn publish_diagnostics<E: Environment>(
.unwrap_or_else(|err| tracing::error!("{err}"));
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn clear_diagnostics<E: Environment>(
mut context: Context<World<E>>,
document_url: Url,
@ -117,7 +117,7 @@ pub(crate) async fn clear_diagnostics<E: Environment>(
.unwrap_or_else(|err| tracing::error!("{}", err));
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
fn collect_syntax_errors(doc: &DocumentState, diags: &mut Vec<Diagnostic>) {
diags.extend(doc.parse.errors.iter().map(|e| {
let range = doc.mapper.range(e.range).unwrap_or_default().into_lsp();
@ -135,7 +135,7 @@ fn collect_syntax_errors(doc: &DocumentState, diags: &mut Vec<Diagnostic>) {
}));
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
fn collect_dom_errors(
doc: &DocumentState,
dom: &Node,
@ -290,7 +290,7 @@ fn collect_dom_errors(
}
}
#[tracing::instrument(level = "debug", skip_all, fields(%document_url))]
#[tracing::instrument(skip_all, fields(%document_url))]
async fn collect_schema_errors<E: Environment>(
ws: &WorkspaceState<E>,
doc: &DocumentState,

View file

@ -1,6 +1,7 @@
use lsp_async_stub::{util::Mapper, Context, Params};
use lsp_types::{
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams,
};
use taplo_common::{
environment::Environment,
@ -12,7 +13,7 @@ use crate::{
world::{DocumentState, World},
};
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn document_open<E: Environment>(
context: Context<World<E>>,
params: Params<DidOpenTextDocumentParams>,
@ -57,7 +58,7 @@ pub(crate) async fn document_open<E: Environment>(
diagnostics::publish_diagnostics(context.clone(), ws_root, p.text_document.uri).await;
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn document_change<E: Environment>(
context: Context<World<E>>,
params: Params<DidChangeTextDocumentParams>,
@ -108,7 +109,15 @@ pub(crate) async fn document_change<E: Environment>(
diagnostics::publish_diagnostics(context.clone(), ws_root, p.text_document.uri).await;
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn document_save<E: Environment>(
_context: Context<World<E>>,
_params: Params<DidSaveTextDocumentParams>,
) {
// stub to silence warnings
}
#[tracing::instrument(skip_all)]
pub(crate) async fn document_close<E: Environment>(
context: Context<World<E>>,
params: Params<DidCloseTextDocumentParams>,

View file

@ -18,7 +18,7 @@ use taplo_common::environment::Environment;
use crate::world::World;
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn folding_ranges<E: Environment>(
context: Context<World<E>>,
params: Params<FoldingRangeParams>,
@ -35,7 +35,7 @@ pub(crate) async fn folding_ranges<E: Environment>(
)))
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub fn create_folding_ranges(syntax: &SyntaxNode, mapper: &Mapper) -> Vec<FoldingRange> {
let mut folding_ranges = Vec::with_capacity(20);

View file

@ -7,15 +7,15 @@ use crate::World;
use lsp_async_stub::{rpc::Error, Context, Params};
use lsp_types::{
CompletionOptions, DocumentLinkOptions, FoldingRangeProviderCapability,
HoverProviderCapability, OneOf, RenameOptions, SemanticTokensFullOptions, SemanticTokensLegend,
SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo,
TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
HoverProviderCapability, InitializedParams, OneOf, RenameOptions, SemanticTokensFullOptions,
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
};
use lsp_types::{InitializeParams, InitializeResult};
use taplo_common::environment::Environment;
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub async fn initialize<E: Environment>(
context: Context<World<E>>,
params: Params<InitializeParams>,
@ -77,11 +77,6 @@ pub async fn initialize<E: Environment>(
range: Some(false),
}),
),
// code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
// code_action_kinds: Some(vec![CodeActionKind::REFACTOR]),
// resolve_provider: None,
// work_done_progress_options: Default::default(),
// })),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
@ -115,3 +110,11 @@ pub async fn initialize<E: Environment>(
offset_encoding: None,
})
}
#[tracing::instrument(skip_all)]
pub async fn initialized<E: Environment>(
_context: Context<World<E>>,
_params: Params<InitializedParams>,
) {
// stub to silence warnings.
}

View file

@ -18,7 +18,7 @@ use taplo::{
};
use taplo_common::environment::Environment;
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub(crate) async fn semantic_tokens<E: Environment>(
context: Context<World<E>>,
params: Params<SemanticTokensParams>,
@ -66,7 +66,7 @@ impl TokenModifier {
pub const MODIFIERS: &'static [SemanticTokenModifier] = &[SemanticTokenModifier::READONLY];
}
#[tracing::instrument(level = "debug", skip_all)]
#[tracing::instrument(skip_all)]
pub fn create_tokens(syntax: &SyntaxNode, mapper: &Mapper) -> Vec<SemanticToken> {
let mut builder = SemanticTokensBuilder::new(mapper);

View file

@ -38,8 +38,10 @@ pub fn create_server<E: Environment>() -> Server<World<E>> {
.on_request::<request::SemanticTokensFullRequest, _>(handlers::semantic_tokens)
.on_request::<request::PrepareRenameRequest, _>(handlers::prepare_rename)
.on_request::<request::Rename, _>(handlers::rename)
.on_notification::<notification::Initialized, _>(handlers::initialized)
.on_notification::<notification::DidOpenTextDocument, _>(handlers::document_open)
.on_notification::<notification::DidChangeTextDocument, _>(handlers::document_change)
.on_notification::<notification::DidSaveTextDocument, _>(handlers::document_save)
.on_notification::<notification::DidCloseTextDocument, _>(handlers::document_close)
.on_notification::<notification::DidChangeConfiguration, _>(handlers::configuration_change)
.on_notification::<notification::DidChangeWorkspaceFolders, _>(handlers::workspace_change)

View file

@ -116,17 +116,25 @@ pub struct WorkspaceState<E: Environment> {
impl<E: Environment> WorkspaceState<E> {
pub(crate) fn new(env: E, root: Url) -> Self {
let client;
#[cfg(target_arch = "wasm32")]
{
client = reqwest::Client::builder().build().unwrap();
}
#[cfg(not(target_arch = "wasm32"))]
{
client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
}
Self {
root,
documents: Default::default(),
taplo_config: Default::default(),
schemas: Schemas::new(
env,
reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap(),
),
schemas: Schemas::new(env, client),
config: LspConfig::default(),
}
}

View file

@ -0,0 +1,41 @@
[package]
name = "taplo-wasm"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.57"
async-trait = "0.1.56"
clap = { version = "3.1.18", features = ["derive"] }
console_error_panic_hook = "0.1.7"
futures = "0.3.21"
js-sys = "0.3.57"
lsp-async-stub = { path = "../lsp-async-stub" }
serde = { version = "1.0.137", features = ["derive"] }
serde-wasm-bindgen = "0.4.3"
serde_json = "1.0.81"
taplo = { path = "../taplo" }
taplo-cli = { path = "../taplo-cli", optional = true }
taplo-common = { path = "../taplo-common" }
taplo-lsp = { path = "../taplo-lsp", optional = true }
time = { version = "0.3.9", features = ["parsing"] }
tokio = "1.19.2"
tracing = "0.1.35"
url = "2.2.2"
wasm-bindgen = { version = "0.2.80" }
wasm-bindgen-futures = "0.4.30"
[features]
default = []
cli = ["taplo-cli"]
lsp = ["taplo-lsp"]
[profile.release]
opt-level = 's'
lto = true

View file

@ -0,0 +1,366 @@
use anyhow::anyhow;
use futures::FutureExt;
use js_sys::{Function, Promise, Uint8Array};
use std::{
io,
path::Path,
pin::Pin,
task::{self, Poll},
};
use taplo_common::environment::Environment;
use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use url::Url;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::{spawn_local, JsFuture};
pub(crate) struct JsAsyncRead {
fut: Option<JsFuture>,
f: Function,
}
impl JsAsyncRead {
fn new(cb: Function) -> Self {
Self { fut: None, f: cb }
}
}
impl AsyncRead for JsAsyncRead {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> task::Poll<std::io::Result<()>> {
if self.fut.is_none() {
let this = JsValue::null();
let ret: JsValue = match self.f.call1(&this, &JsValue::from(buf.remaining())) {
Ok(val) => val,
Err(error) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
format!("{:?}", error),
)));
}
};
let promise = match Promise::try_from(ret) {
Ok(p) => p,
Err(err) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
format!("{:?}", err),
)));
}
};
self.fut = Some(JsFuture::from(promise));
}
if let Some(fut) = self.fut.as_mut() {
match fut.poll_unpin(cx) {
task::Poll::Ready(val) => {
let res = match val {
Ok(chunk) => {
let arr = js_sys::Uint8Array::from(chunk).to_vec();
if !arr.is_empty() {
buf.put_slice(&arr);
}
Ok(())
}
Err(err) => Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))),
};
self.fut = None;
Poll::Ready(res)
}
task::Poll::Pending => Poll::Pending,
}
} else {
unreachable!()
}
}
}
impl JsAsyncWrite {
fn new(cb: Function) -> Self {
Self { fut: None, f: cb }
}
}
pub(crate) struct JsAsyncWrite {
fut: Option<JsFuture>,
f: Function,
}
impl AsyncWrite for JsAsyncWrite {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &[u8],
) -> task::Poll<Result<usize, std::io::Error>> {
if self.fut.is_none() {
let this = JsValue::null();
let ret: JsValue = match self.f.call1(&this, &Uint8Array::from(buf).into()) {
Ok(val) => val,
Err(error) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
format!("{:?}", error),
)));
}
};
let promise = match Promise::try_from(ret) {
Ok(p) => p,
Err(err) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
format!("{:?}", err),
)));
}
};
self.fut = Some(JsFuture::from(promise));
}
if let Some(fut) = self.fut.as_mut() {
match fut.poll_unpin(cx) {
task::Poll::Ready(val) => {
let res = match val {
Ok(num_written) => {
let n = num_written.as_f64().unwrap_or(0.0).floor() as usize;
Ok(n)
}
Err(err) => Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))),
};
self.fut = None;
Poll::Ready(res)
}
task::Poll::Pending => Poll::Pending,
}
} else {
unreachable!()
}
}
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut task::Context<'_>,
) -> task::Poll<Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: Pin<&mut Self>,
_cx: &mut task::Context<'_>,
) -> task::Poll<Result<(), std::io::Error>> {
Poll::Ready(Ok(()))
}
}
#[derive(Clone)]
pub(crate) struct WasmEnvironment {
js_now: Function,
js_env_var: Function,
js_atty_stderr: Function,
js_on_stdin: Function,
js_on_stdout: Function,
js_on_stderr: Function,
js_glob_files: Function,
js_read_file: Function,
js_write_file: Function,
js_to_file_path: Function,
js_is_absolute: Function,
js_cwd: Function,
js_find_config_file: Function,
}
impl From<JsValue> for WasmEnvironment {
fn from(val: JsValue) -> Self {
Self {
js_now: js_sys::Reflect::get(&val, &JsValue::from_str("js_now"))
.unwrap()
.into(),
js_env_var: js_sys::Reflect::get(&val, &JsValue::from_str("js_env_var"))
.unwrap()
.into(),
js_atty_stderr: js_sys::Reflect::get(&val, &JsValue::from_str("js_atty_stderr"))
.unwrap()
.into(),
js_on_stdin: js_sys::Reflect::get(&val, &JsValue::from_str("js_on_stdin"))
.unwrap()
.into(),
js_on_stdout: js_sys::Reflect::get(&val, &JsValue::from_str("js_on_stdout"))
.unwrap()
.into(),
js_on_stderr: js_sys::Reflect::get(&val, &JsValue::from_str("js_on_stderr"))
.unwrap()
.into(),
js_glob_files: js_sys::Reflect::get(&val, &JsValue::from_str("js_glob_files"))
.unwrap()
.into(),
js_read_file: js_sys::Reflect::get(&val, &JsValue::from_str("js_read_file"))
.unwrap()
.into(),
js_write_file: js_sys::Reflect::get(&val, &JsValue::from_str("js_write_file"))
.unwrap()
.into(),
js_to_file_path: js_sys::Reflect::get(&val, &JsValue::from_str("js_to_file_path"))
.unwrap()
.into(),
js_is_absolute: js_sys::Reflect::get(&val, &JsValue::from_str("js_is_absolute"))
.unwrap()
.into(),
js_cwd: js_sys::Reflect::get(&val, &JsValue::from_str("js_cwd"))
.unwrap()
.into(),
js_find_config_file: js_sys::Reflect::get(
&val,
&JsValue::from_str("js_find_config_file"),
)
.unwrap()
.into(),
}
}
}
// SAFETY: we're in a single-threaded WASM environment.
unsafe impl Send for WasmEnvironment {}
unsafe impl Sync for WasmEnvironment {}
#[async_trait::async_trait(?Send)]
impl Environment for WasmEnvironment {
type Stdin = JsAsyncRead;
type Stdout = JsAsyncWrite;
type Stderr = JsAsyncWrite;
fn now(&self) -> OffsetDateTime {
let this = JsValue::null();
let res: JsValue = self.js_now.call0(&this).unwrap();
let s: String = js_sys::Date::from(res).to_iso_string().into();
OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).unwrap()
}
fn spawn<F>(&self, fut: F)
where
F: std::future::Future + Send + 'static,
F::Output: Send,
{
spawn_local(async move {
fut.await;
})
}
fn spawn_local<F>(&self, fut: F)
where
F: std::future::Future + 'static,
{
spawn_local(async move {
fut.await;
})
}
fn env_var(&self, name: &str) -> Option<String> {
let this = JsValue::null();
let res: JsValue = self
.js_env_var
.call1(&this, &JsValue::from_str(name))
.unwrap();
res.as_string()
}
fn atty_stderr(&self) -> bool {
let this = JsValue::null();
let res: JsValue = self.js_atty_stderr.call0(&this).unwrap();
res.as_bool().unwrap_or(false)
}
fn stdin(&self) -> Self::Stdin {
JsAsyncRead::new(self.js_on_stdin.clone())
}
fn stdout(&self) -> Self::Stdout {
JsAsyncWrite::new(self.js_on_stdout.clone())
}
fn stderr(&self) -> Self::Stderr {
JsAsyncWrite::new(self.js_on_stderr.clone())
}
fn glob_files(&self, glob: &str) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
let this = JsValue::null();
let res: JsValue = self
.js_glob_files
.call1(&this, &JsValue::from_str(glob))
.unwrap();
serde_wasm_bindgen::from_value(res).map_err(|err| anyhow!("{err}"))
}
async fn read_file(&self, path: &Path) -> Result<Vec<u8>, anyhow::Error> {
let path_str = JsValue::from_str(&path.to_string_lossy());
let this = JsValue::null();
let res: JsValue = self.js_read_file.call1(&this, &path_str).unwrap();
let ret = JsFuture::from(Promise::from(res))
.await
.map_err(|err| anyhow!("{:?}", err))?;
Ok(Uint8Array::from(ret).to_vec())
}
async fn write_file(&self, path: &Path, bytes: &[u8]) -> Result<(), anyhow::Error> {
let path_str = JsValue::from_str(&path.to_string_lossy());
let this = JsValue::null();
let res: JsValue = self
.js_write_file
.call2(&this, &path_str, &JsValue::from(Uint8Array::from(bytes)))
.unwrap();
Ok(serde_wasm_bindgen::from_value(
JsFuture::from(Promise::from(res))
.await
.map_err(|err| anyhow!("{:?}", err))?,
)
.map_err(|err| anyhow!("{err}"))?)
}
fn to_file_path(&self, url: &Url) -> Option<std::path::PathBuf> {
let url_str = JsValue::from_str(url.as_str());
let this = JsValue::null();
let res: JsValue = self.js_to_file_path.call1(&this, &url_str).unwrap();
res.as_string().map(Into::into)
}
fn is_absolute(&self, path: &Path) -> bool {
let path_str = JsValue::from_str(&path.to_string_lossy());
let this = JsValue::null();
let res: JsValue = self.js_is_absolute.call1(&this, &path_str).unwrap();
res.is_truthy()
}
fn cwd(&self) -> Option<std::path::PathBuf> {
let this = JsValue::null();
let res: JsValue = self.js_cwd.call0(&this).unwrap();
res.as_string().map(Into::into)
}
async fn find_config_file(&self, from: &Path) -> Option<std::path::PathBuf> {
let path_str = JsValue::from_str(&from.to_string_lossy());
let this = JsValue::null();
let res: JsValue = self.js_find_config_file.call1(&this, &path_str).unwrap();
if res.is_undefined() {
return None;
}
res.as_string().map(Into::into)
}
}

View file

@ -0,0 +1,226 @@
use environment::WasmEnvironment;
use serde::Serialize;
use std::path::Path;
use taplo::{dom::Node, formatter, parser::parse};
use taplo_common::{config::Config, schema::Schemas};
use url::Url;
use wasm_bindgen::prelude::*;
mod environment;
#[cfg(feature = "lsp")]
mod lsp;
#[derive(Serialize)]
struct Range {
start: u32,
end: u32,
}
#[derive(Serialize)]
struct LintError {
#[serde(skip_serializing_if = "Option::is_none")]
range: Option<Range>,
error: String,
}
#[derive(Serialize)]
struct LintResult {
errors: Vec<LintError>,
}
#[wasm_bindgen]
pub fn initialize() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub fn format(
env: JsValue,
toml: &str,
options: JsValue,
config: JsValue,
) -> Result<String, JsError> {
let mut config = if config.is_undefined() {
Config::default()
} else {
serde_wasm_bindgen::from_value(config)?
};
let env = WasmEnvironment::from(env);
config
.prepare(&env, Path::new("/"))
.map_err(|err| JsError::new(&err.to_string()))?;
let camel_opts: formatter::OptionsIncompleteCamel = serde_wasm_bindgen::from_value(options)?;
let mut options = formatter::Options::default();
if let Some(cfg_opts) = config.global_options.formatting.clone() {
options.update(cfg_opts);
}
options.update_camel(camel_opts);
let syntax = parse(toml);
let error_ranges = syntax.errors.iter().map(|e| e.range).collect::<Vec<_>>();
Ok(formatter::format_with_path_scopes(
syntax.into_dom(),
options,
&error_ranges,
config.format_scopes(Path::new("")),
)?)
}
#[wasm_bindgen]
pub async fn lint(env: JsValue, toml: String, config: JsValue) -> Result<JsValue, JsError> {
let mut config = if config.is_undefined() {
Config::default()
} else {
serde_wasm_bindgen::from_value(config)?
};
let env = WasmEnvironment::from(env);
config
.prepare(&env, Path::new("/"))
.map_err(|err| JsError::new(&err.to_string()))?;
let syntax = parse(&toml);
if !syntax.errors.is_empty() {
return Ok(serde_wasm_bindgen::to_value(&LintResult {
errors: syntax
.errors
.into_iter()
.map(|err| LintError {
range: Range {
start: err.range.start().into(),
end: err.range.end().into(),
}
.into(),
error: err.to_string(),
})
.collect(),
})?);
}
let dom = syntax.into_dom();
if let Err(errors) = dom.validate() {
return Ok(serde_wasm_bindgen::to_value(&LintResult {
errors: errors
.map(|err| LintError {
range: None,
error: err.to_string(),
})
.collect(),
})?);
}
let schemas = Schemas::new(env, Default::default());
schemas.associations().add_from_config(&config);
if let Some(schema) = schemas
.associations()
.association_for(&Url::parse("file:///__.toml").unwrap())
{
let schema_errors = schemas
.validate(&schema.url, &serde_json::to_value(&dom).unwrap())
.await
.map_err(|err| JsError::new(&err.to_string()))?;
return Ok(serde_wasm_bindgen::to_value(&LintResult {
errors: schema_errors
.into_iter()
.map(|err| LintError {
range: None,
error: err.to_string(),
})
.collect(),
})?);
}
todo!()
}
#[wasm_bindgen]
pub fn to_json(toml: &str) -> Result<String, JsError> {
let syntax = parse(toml);
if !syntax.errors.is_empty() {
return Err(JsError::new("the given input contains syntax errors"));
}
let dom = syntax.into_dom();
if dom.validate().is_err() {
return Err(JsError::new("the given input contains errors"));
}
Ok(serde_json::to_string(&dom).unwrap())
}
#[wasm_bindgen]
pub fn from_json(json: &str) -> Result<String, JsError> {
let dom: Node = serde_json::from_str(json)?;
Ok(dom.to_toml(false))
}
#[cfg(feature = "cli")]
#[wasm_bindgen]
pub async fn run_cli(env: JsValue, args: JsValue) -> Result<(), JsError> {
use clap::Parser;
use environment::WasmEnvironment;
use taplo_cli::{
args::{Colors, TaploArgs},
Taplo,
};
use taplo_common::{environment::Environment, log::setup_stderr_logging};
use tokio::io::AsyncWriteExt;
use tracing::Instrument;
let env = WasmEnvironment::from(env);
let args: Vec<String> = serde_wasm_bindgen::from_value(args)?;
let cli = match TaploArgs::try_parse_from(args) {
Ok(v) => v,
Err(error) => {
env.stdout().write_all(error.to_string().as_bytes()).await?;
return Err(JsError::new("operation failed"));
}
};
setup_stderr_logging(
env.clone(),
cli.log_spans,
cli.verbose,
match cli.colors {
Colors::Auto => None,
Colors::Always => Some(true),
Colors::Never => Some(false),
},
);
match Taplo::new(env.clone())
.execute(cli)
.instrument(tracing::info_span!("taplo"))
.await
{
Ok(_) => Ok(()),
Err(error) => {
tracing::error!(error = %format!("{error:#}"), "operation failed");
Err(JsError::new("operation failed"))
}
}
}
#[cfg(feature = "lsp")]
#[wasm_bindgen]
pub async fn create_lsp(env: JsValue, lsp_interface: JsValue) -> lsp::TaploWasmLsp {
use taplo_common::log::setup_stderr_logging;
let env = WasmEnvironment::from(env);
setup_stderr_logging(env.clone(), false, false, None);
lsp::TaploWasmLsp {
server: taplo_lsp::create_server(),
world: taplo_lsp::create_world(env),
lsp_interface: lsp::WasmLspInterface::from(lsp_interface),
}
}

View file

@ -0,0 +1,85 @@
use crate::environment::WasmEnvironment;
use futures::Sink;
use js_sys::Function;
use lsp_async_stub::{rpc, Server};
use std::{io, sync::Arc};
use taplo_lsp::world::WorldState;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
#[wasm_bindgen]
pub struct TaploWasmLsp {
pub(crate) server: Server<Arc<WorldState<WasmEnvironment>>>,
pub(crate) world: Arc<WorldState<WasmEnvironment>>,
pub(crate) lsp_interface: WasmLspInterface,
}
#[wasm_bindgen]
impl TaploWasmLsp {
pub fn send(&self, message: JsValue) -> Result<(), JsError> {
let message: lsp_async_stub::rpc::Message = serde_wasm_bindgen::from_value(message)?;
let world = self.world.clone();
let writer = self.lsp_interface.clone();
let msg_fut = self.server.handle_message(world, message, writer);
spawn_local(async move {
if let Err(err) = msg_fut.await {
tracing::error!(error = %err, "lsp message error");
}
});
Ok(())
}
}
#[derive(Clone)]
pub(crate) struct WasmLspInterface {
js_on_message: Function,
}
impl From<JsValue> for WasmLspInterface {
fn from(val: JsValue) -> Self {
Self {
js_on_message: js_sys::Reflect::get(&val, &JsValue::from_str("js_now"))
.unwrap()
.into(),
}
}
}
impl Sink<rpc::Message> for WasmLspInterface {
type Error = io::Error;
fn poll_ready(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn start_send(
self: std::pin::Pin<&mut Self>,
message: rpc::Message,
) -> Result<(), Self::Error> {
let this = JsValue::null();
self.js_on_message
.call1(&this, &serde_wasm_bindgen::to_value(&message).unwrap())
.unwrap();
Ok(())
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_close(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
}

View file

@ -4,3 +4,11 @@ node_modules
.vscode-test/
*.vsix
.vscode
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules

View file

@ -11,4 +11,7 @@ tsconfig.json
tslint.json
sample
images
node_modules
node_modules
.yarn/**
.yarnrc.yml
rollup-config.js

786
editors/vscode/.yarn/releases/yarn-3.2.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -1,5 +1,20 @@
# Change Log
## next
### Features
- Support for `tomlValidation` for schemas.
- Wide-range JSON schema support
- Support for schema store
- Support for multiple workspaces
- It is now possible to specify schemas with either `#:schema <URL>` or `$schema = "<URL>"` in TOML files
- Added rename feature that lets you rename keys that appear at multiple locations
### Other
- Almost a complete internal rewrite with various fixes and improvements. ([#211](https://github.com/tamasfe/taplo/pull/211))
## 0.14.3
### Fixes

View file

@ -21,7 +21,7 @@
},
"license": "SEE LICENSE IN LICENSE.md",
"engines": {
"vscode": "^1.44.0"
"vscode": "^1.68.0"
},
"categories": [
"Programming Languages",
@ -158,8 +158,14 @@
"configuration": {
"title": "Even Better TOML",
"properties": {
"evenBetterToml.taplo.bundled": {
"description": "Use the bundled taplo language server. If set to `false`, the `taplo` executable must be found in PATH or must be set in `evenBetterToml.taplo.path`.",
"type": "boolean",
"scope": "resource",
"default": true
},
"evenBetterToml.taplo.path": {
"description": "An absolute path to the `taplo` executable.",
"description": "An absolute path to the `taplo` executable. `evenBetterToml.taplo.bundled` needs to be set to `false` for this to have any effect.",
"type": [
"string",
"null"
@ -172,7 +178,7 @@
"object"
],
"scope": "resource",
"description": "Environment variables for the Taplo LSP executable.",
"description": "Environment variables for Taplo.",
"additionalProperties": {
"type": "string"
},
@ -184,7 +190,7 @@
"null"
],
"scope": "resource",
"description": "Additional arguments for the Taplo LSP executable.",
"description": "Additional arguments for Taplo. Has no effect for the bundled LSP.",
"items": {
"type": "string"
},
@ -272,25 +278,29 @@
"main": "./dist/extension.js",
"scripts": {
"build:syntax": "ts-node src/syntax/index.ts",
"vscode:prepublish": "yarn --ignore-engines build",
"build": "yarn --ignore-engines rollup --silent -c rollup.config.js"
"vscode:prepublish": "yarn build",
"build": "rm -rf dist && rollup -c rollup.config.js"
},
"dependencies": {
"deep-equal": "^2.0.4",
"@taplo/lsp": "^0.3.0-alpha.4",
"deep-equal": "^2.0.5",
"encoding": "^0.1.13",
"vscode-languageclient": "^7.0.0",
"fast-glob": "^3.2.11",
"node-fetch": "^3.2.6",
"vscode-languageclient": "^8.0.1",
"which": "^2.0.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@types/deep-equal": "^1.0.1",
"@types/node": "^12.12.0",
"@types/vscode": "^1.44.0",
"@types/node": "^17.0.42",
"@types/vscode": "^1.68.0",
"@types/which": "^2.0.1",
"rollup": "^2.33.1",
"rollup-plugin-typescript2": "^0.29.0",
"ts-node": "^8.10.2",
"typescript": "^4.0.5"
"rollup": "^2.75.6",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.32.1",
"ts-node": "^10.8.1",
"typescript": "^4.7.3"
}
}

View file

@ -1,20 +1,51 @@
import typescript from "rollup-plugin-typescript2";
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import { terser } from "rollup-plugin-terser";
import path from "node:path";
export default {
const onwarn = (warning, rollupWarn) => {
const ignoredWarnings = [
{
ignoredCode: "CIRCULAR_DEPENDENCY",
ignoredPath: "node_modules/semver",
},
];
// only show warning when code and path don't match
// anything in above list of ignored warnings
if (
!ignoredWarnings.some(
({ ignoredCode, ignoredPath }) =>
warning.code === ignoredCode &&
warning.importer.includes(path.normalize(ignoredPath))
)
) {
rollupWarn(warning);
}
};
/** @type {import('rollup').RollupOptions} */
const options = {
onwarn,
input: {
// server: "src/server.ts",
server: "src/server.ts",
extension: "src/extension.ts",
},
output: {
sourcemap: false,
format: "cjs",
dir: "dist",
chunkFileNames: "[name].js",
},
external: ["vscode"],
preserveEntrySignatures: true,
plugins: [
commonjs(),
resolve({ preferBuiltins: true }),
typescript(),
commonjs({ include: ["src/*.ts", "node_modules/**", "../lsp/**"] }),
resolve({ jsnext: true, preferBuiltins: true }),
terser(),
],
};
export default options;

View file

@ -1,55 +1,71 @@
import * as vscode from "vscode";
import * as client from "vscode-languageclient/node";
import * as path from "path";
import * as fs from "fs";
import which from "which";
import { getOutput } from "./extension";
import { getOutput } from "./util";
export async function createClient(
context: vscode.ExtensionContext
): Promise<client.LanguageClient> {
return createNativeClient(context);
}
const out = getOutput();
async function createNativeClient(
context: vscode.ExtensionContext
): Promise<client.LanguageClient> {
const taploPath =
vscode.workspace.getConfiguration().get("evenBetterToml.taplo.path") ??
which.sync("taplo", { nothrow: true });
if (typeof taploPath !== "string") {
getOutput().appendLine("failed to locate Taplo LSP");
throw new Error("failed to locate Taplo LSP");
}
let extraArgs = vscode.workspace
const bundled = !!vscode.workspace
.getConfiguration()
.get("evenBetterToml.taplo.extraArgs");
.get("evenBetterToml.taplo.bundled");
if (!Array.isArray(extraArgs)) {
extraArgs = [];
let serverOpts: client.ServerOptions;
if (bundled) {
const taploPath = context.asAbsolutePath(path.join("dist", "server.js"));
const run: client.NodeModule = {
module: taploPath,
transport: client.TransportKind.ipc,
};
serverOpts = {
run,
debug: run,
};
} else {
const taploPath =
vscode.workspace.getConfiguration().get("evenBetterToml.taplo.path") ??
which.sync("taplo", { nothrow: true });
if (typeof taploPath !== "string") {
out.appendLine("failed to locate Taplo LSP");
throw new Error("failed to locate Taplo LSP");
}
let extraArgs = vscode.workspace
.getConfiguration()
.get("evenBetterToml.taplo.extraArgs");
if (!Array.isArray(extraArgs)) {
extraArgs = [];
}
const args: string[] = (extraArgs as any[]).filter(
a => typeof a === "string"
);
const run: client.Executable = {
command: taploPath,
args: ["lsp", "stdio", ...args],
options: {
env:
vscode.workspace
.getConfiguration()
.get("evenBetterToml.taplo.environment") ?? undefined,
},
};
serverOpts = {
run,
debug: run,
};
}
const args: string[] = (extraArgs as any[]).filter(
a => typeof a === "string"
);
const run: client.Executable = {
command: taploPath,
args: ["lsp", "stdio", ...args],
options: {
env:
vscode.workspace
.getConfiguration()
.get("evenBetterToml.taplo.environment") ?? undefined,
},
};
let serverOpts: client.ServerOptions = {
run,
debug: run,
};
return new client.LanguageClient(
"evenBetterToml",
"Even Better TOML LSP",

View file

@ -1,41 +1,54 @@
import * as vscode from "vscode";
import * as client from "vscode-languageclient/node";
import { getOutput } from "../extension";
import { getOutput } from "../util";
// FIXME: this could be a bit more DRY.
export function register(
ctx: vscode.ExtensionContext,
c: client.LanguageClient
) {
c.onReady().then(() => {
ctx.subscriptions.push(
vscode.commands.registerTextEditorCommand(
"evenBetterToml.copyAsJson",
async editor => {
const document = editor.document;
// Avoid accidental copying of nothing
if (editor.selection.isEmpty) {
return;
}
ctx.subscriptions.push(
vscode.commands.registerTextEditorCommand(
"evenBetterToml.copyAsJson",
async editor => {
const document = editor.document;
// Avoid accidental copying of nothing
if (editor.selection.isEmpty) {
return;
}
const selectedText = document.getText(editor.selection);
// Avoid accidental copying of nothing
if (selectedText.trim().length === 0) {
return;
}
const selectedText = document.getText(editor.selection);
// Avoid accidental copying of nothing
if (selectedText.trim().length === 0) {
return;
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToJson",
{
text: selectedText,
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToJson",
{
text: selectedText,
}
);
const out = getOutput();
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert TOML to JSON: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
const out = getOutput();
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert TOML to JSON: ${res.error}`);
if (show) {
out.show();
}
return;
}
try {
if (!res.text) {
out.appendLine(`The response shouldn't be empty, but it is.`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
@ -46,23 +59,64 @@ export function register(
}
return;
}
await vscode.env.clipboard.writeText(res.text);
} catch (e) {
out.appendLine(`Couldn't write to clipboard: ${e}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
try {
if (!res.text) {
out.appendLine(`The response shouldn't be empty, but it is.`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
if (show) {
out.show();
}
return;
}
await vscode.env.clipboard.writeText(res.text);
} catch (e) {
out.appendLine(`Couldn't write to clipboard: ${e}`);
await vscode.window.showInformationMessage("JSON copied!");
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.copyAsToml",
async editor => {
const document = editor.document;
// Avoid accidental copying of nothing
if (editor.selection.isEmpty) {
return;
}
const selectedText = document.getText(editor.selection);
// Avoid accidental copying of nothing
if (selectedText.trim().length === 0) {
return;
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToToml",
{
text: selectedText,
}
);
const out = getOutput();
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert JSON to TOML: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
try {
if (!res.text) {
out.appendLine(`The response shouldn't be empty, but it is.`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
@ -73,170 +127,114 @@ export function register(
}
return;
}
await vscode.window.showInformationMessage("JSON copied!");
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.copyAsToml",
async editor => {
const document = editor.document;
// Avoid accidental copying of nothing
if (editor.selection.isEmpty) {
return;
}
const selectedText = document.getText(editor.selection);
// Avoid accidental copying of nothing
if (selectedText.trim().length === 0) {
return;
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToToml",
{
text: selectedText,
}
await vscode.env.clipboard.writeText(res.text);
} catch (e) {
out.appendLine(`Couldn't write to clipboard: ${e}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
const out = getOutput();
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert JSON to TOML: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
if (show) {
out.show();
}
try {
if (!res.text) {
out.appendLine(`The response shouldn't be empty, but it is.`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
await vscode.env.clipboard.writeText(res.text);
} catch (e) {
out.appendLine(`Couldn't write to clipboard: ${e}`);
const show = await vscode.window.showErrorMessage(
"Copying has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
await vscode.window.showInformationMessage("TOML copied!");
return;
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.pasteAsJson",
async editor => {
const out = getOutput();
let input: string;
try {
input = await vscode.env.clipboard.readText();
} catch (e) {
out.appendLine(`Failed to read from clipboard:${e}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToJson",
{
text: input,
}
await vscode.window.showInformationMessage("TOML copied!");
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.pasteAsJson",
async editor => {
const out = getOutput();
let input: string;
try {
input = await vscode.env.clipboard.readText();
} catch (e) {
out.appendLine(`Failed to read from clipboard:${e}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert to JSON: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Pasting JSON has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
if (show) {
out.show();
}
editor.edit(e => {
e.replace(editor.selection, res.text!);
});
return;
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.pasteAsToml",
async editor => {
const out = getOutput();
let input: string;
try {
input = await vscode.env.clipboard.readText();
} catch (e) {
out.appendLine(`Failed to read from clipboard:${e}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToJson",
{
text: input,
}
);
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToToml",
{
text: input,
}
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert to JSON: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Pasting JSON has failed!",
"Show Details"
);
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert to TOML: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
if (show) {
out.show();
}
editor.edit(e => {
e.replace(editor.selection, res.text!);
});
return;
}
)
);
});
editor.edit(e => {
e.replace(editor.selection, res.text!);
});
}
),
vscode.commands.registerTextEditorCommand(
"evenBetterToml.pasteAsToml",
async editor => {
const out = getOutput();
let input: string;
try {
input = await vscode.env.clipboard.readText();
} catch (e) {
out.appendLine(`Failed to read from clipboard:${e}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
const res = await c.sendRequest<{ text?: string; error?: string }>(
"taplo/convertToToml",
{
text: input,
}
);
if (res.error?.length ?? 0 !== 0) {
out.appendLine(`Failed to convert to TOML: ${res.error}`);
const show = await vscode.window.showErrorMessage(
"Paste from clipboard has failed!",
"Show Details"
);
if (show) {
out.show();
}
return;
}
editor.edit(e => {
e.replace(editor.selection, res.text!);
});
}
)
);
}

View file

@ -1,58 +1,55 @@
import * as vscode from "vscode";
import * as client from "vscode-languageclient/node";
import { getOutput } from "../extension";
export function register(
ctx: vscode.ExtensionContext,
c: client.LanguageClient
) {
c.onReady().then(() => {
ctx.subscriptions.push(
vscode.commands.registerTextEditorCommand(
"evenBetterToml.selectSchema",
async editor => {
const schemasResp: { schemas: { url: string; meta?: any }[] } =
await c.sendRequest("taplo/listSchemas", {
documentUri: editor.document.uri.toString(),
});
interface SchemaItem extends vscode.QuickPickItem {
url: string;
meta?: Record<string, any>;
}
const selectedSchema: { schema?: { url: string } } =
await c.sendRequest("taplo/associatedSchema", {
documentUri: editor.document.uri.toString(),
});
const selection = await vscode.window.showQuickPick<SchemaItem>(
schemasResp.schemas.map(s => ({
label: s.meta?.name ?? s.url,
description: schemaDescription(s.meta),
detail: schemaDetails(s.url, s.meta),
picked: selectedSchema.schema?.url === s.url,
url: s.url,
meta: s.meta,
}))
);
if (!selection) {
return;
}
c.sendNotification("taplo/associateSchema", {
ctx.subscriptions.push(
vscode.commands.registerTextEditorCommand(
"evenBetterToml.selectSchema",
async editor => {
const schemasResp: { schemas: { url: string; meta?: any }[] } =
await c.sendRequest("taplo/listSchemas", {
documentUri: editor.document.uri.toString(),
schemaUri: selection.url,
rule: {
url: editor.document.uri.toString(),
},
meta: selection.meta,
});
interface SchemaItem extends vscode.QuickPickItem {
url: string;
meta?: Record<string, any>;
}
)
);
});
const selectedSchema: { schema?: { url: string } } =
await c.sendRequest("taplo/associatedSchema", {
documentUri: editor.document.uri.toString(),
});
const selection = await vscode.window.showQuickPick<SchemaItem>(
schemasResp.schemas.map(s => ({
label: s.meta?.name ?? s.url,
description: schemaDescription(s.meta),
detail: schemaDetails(s.url, s.meta),
picked: selectedSchema.schema?.url === s.url,
url: s.url,
meta: s.meta,
}))
);
if (!selection) {
return;
}
c.sendNotification("taplo/associateSchema", {
documentUri: editor.document.uri.toString(),
schemaUri: selection.url,
rule: {
url: editor.document.uri.toString(),
},
meta: selection.meta,
});
}
)
);
}
function schemaDescription(meta: any | undefined): string | undefined {

View file

@ -3,16 +3,7 @@ import * as client from "vscode-languageclient/node";
import { registerCommands } from "./commands";
import { createClient } from "./client";
import { syncExtensionSchemas } from "./tomlValidation";
let output: vscode.OutputChannel;
export function getOutput(): vscode.OutputChannel {
if (!output) {
output = vscode.window.createOutputChannel("Even Better TOML");
}
return output;
}
import { getOutput } from "./util";
export async function activate(context: vscode.ExtensionContext) {
const schemaIndicator = vscode.window.createStatusBarItem(
@ -25,9 +16,7 @@ export async function activate(context: vscode.ExtensionContext) {
schemaIndicator.command = "evenBetterToml.selectSchema";
const c = await createClient(context);
context.subscriptions.push(c.start());
await c.onReady();
await c.start();
if (vscode.window.activeTextEditor?.document.languageId === "toml") {
schemaIndicator.show();
@ -37,7 +26,7 @@ export async function activate(context: vscode.ExtensionContext) {
syncExtensionSchemas(context, c);
context.subscriptions.push(
output,
getOutput(),
schemaIndicator,
c.onNotification("taplo/messageWithOutput", async params =>
showMessage(params, c)

View file

@ -0,0 +1,63 @@
import fs from "fs";
import fsPromise from "fs/promises";
import path from "path";
import { exit } from "process";
import { RpcMessage, TaploLsp } from "@taplo/lsp";
import fetch, { Headers, Request, Response } from "node-fetch";
import glob from "fast-glob";
let taplo: TaploLsp;
process.on("message", async (d: RpcMessage) => {
if (d.method === "exit") {
exit(0);
}
if (typeof taplo === "undefined") {
taplo = await TaploLsp.initialize(
{
cwd: () => process.cwd(),
envVar: name => process.env[name],
findConfigFile: from => {
const fileNames = [".taplo.toml", "taplo.toml"];
for (const name of fileNames) {
try {
const fullPath = path.join(from, name);
fs.accessSync(fullPath);
return fullPath;
} catch {}
}
},
glob: p => glob.sync(p),
isAbsolute: p => path.isAbsolute(p),
now: () => new Date(),
readFile: path => fsPromise.readFile(path),
writeFile: (path, content) => fsPromise.writeFile(path, content),
stderr: process.stderr,
stdErrAtty: () => process.stderr.isTTY,
stdin: process.stdin,
stdout: process.stdout,
urlToFilePath: (url: string) => decodeURI(url).slice("file://".length),
fetch: {
fetch,
Headers,
Request,
Response,
},
},
{
onMessage(message) {
process.send(message);
},
}
);
}
taplo.send(d);
});
// These are panics from Rust.
process.on("unhandledRejection", up => {
throw up;
});

View file

@ -1,5 +1,15 @@
import * as vscode from "vscode";
let output: vscode.OutputChannel;
export function getOutput(): vscode.OutputChannel {
if (!output) {
output = vscode.window.createOutputChannel("Even Better TOML");
}
return output;
}
export function allRange(doc: vscode.TextDocument): vscode.Range {
let firstLine = doc.lineAt(0);
let lastLine = doc.lineAt(doc.lineCount - 1);

File diff suppressed because it is too large Load diff

8
js/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules

786
js/.yarn/releases/yarn-stable-temp.cjs vendored Executable file

File diff suppressed because one or more lines are too long

2
js/.yarnrc.yml Normal file
View file

@ -0,0 +1,2 @@
nodeLinker: node-modules
npmPublishAccess: public

5
js/cli/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
dist
node_modules
target
**/.local*
*.tgz

8
js/cli/.npmignore Normal file
View file

@ -0,0 +1,8 @@
src
target
util
**/.local*
*.tgz
tsconfig.json
rollup.config.js
yarn-error.log

3
js/cli/README.md Normal file
View file

@ -0,0 +1,3 @@
This is a JavaScript wrapper for the Taplo TOML CLI tool.
All the documentation and information is available on the [website](https://taplo.tamasfe.dev).

34
js/cli/package.json Normal file
View file

@ -0,0 +1,34 @@
{
"name": "@taplo/cli",
"version": "0.4.0",
"description": "A TOML utility tool.",
"bin": {
"taplo": "dist/cli.js"
},
"scripts": {
"build": "yarn rollup --silent -c rollup.config.js && node util/add_shebang.js",
"prepublish": "RELEASE=true yarn build"
},
"homepage": "https://taplo.tamasfe.dev",
"repository": "https://github.com/tamasfe/taplo",
"author": {
"name": "tamasfe",
"url": "https://github.com/tamasfe"
},
"license": "MIT",
"devDependencies": {
"@taplo/core": "^0.1.0",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@types/node": "^17.0.41",
"@types/node-fetch": "^2.6.1",
"@wasm-tool/rollup-plugin-rust": "^2.2.2",
"fast-glob": "^3.2.11",
"node-fetch": "^3.2.6",
"rollup": "^2.75.6",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.32.1",
"tslib": "^2.4.0",
"typescript": "^4.7.3"
}
}

36
js/cli/rollup.config.js Normal file
View file

@ -0,0 +1,36 @@
import rust from "@wasm-tool/rollup-plugin-rust";
import typescript from "rollup-plugin-typescript2";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { terser } from "rollup-plugin-terser";
import path from "path";
import process from "process";
export default {
input: {
cli: "src/cli.ts",
},
output: {
sourcemap: false,
format: "cjs",
dir: "dist",
},
preserveEntrySignatures: false,
inlineDynamicImports: true,
plugins: [
rust({
debug: process.env["RELEASE"] !== "true",
nodejs: true,
inlineWasm: true,
cargoArgs: ["--features", "cli"],
}),
commonjs(),
resolve({
jsnext: true,
preferBuiltins: true,
rootDir: path.join(process.cwd(), ".."),
}),
typescript(),
terser(),
],
};

52
js/cli/src/cli.ts Normal file
View file

@ -0,0 +1,52 @@
import fsPromise from "fs/promises";
import fs from "fs";
import process from "process";
import path from "path";
import glob from "fast-glob";
import fetch, { Headers, Request, Response } from "node-fetch";
import loadTaplo from "../../../crates/taplo-wasm/Cargo.toml";
import { convertEnv, Environment, prepareEnv } from "@taplo/core";
(async function main() {
const taplo = await loadTaplo();
taplo.initialize();
const env: Environment = {
cwd: () => process.cwd(),
envVar: name => process.env[name],
findConfigFile: from => {
try {
fs.accessSync(path.join(from, ".taplo.toml"));
return path.join(from, ".taplo.toml");
} catch {}
try {
fs.accessSync(path.join(from, "taplo.toml"));
return path.join(from, "taplo.toml");
} catch {}
},
glob: p => glob.sync(p),
isAbsolute: p => path.isAbsolute(p),
now: () => new Date(),
readFile: path => fsPromise.readFile(path),
writeFile: (path, content) => fsPromise.writeFile(path, content),
stderr: process.stderr,
stdErrAtty: () => process.stderr.isTTY,
stdin: process.stdin,
stdout: process.stdout,
urlToFilePath: (url: string) => decodeURI(url).slice("file://".length),
fetch: {
fetch,
Headers,
Request,
Response,
},
};
prepareEnv(env);
try {
await taplo.run_cli(convertEnv(env), process.argv.slice(1));
} catch (err) {
process.exitCode = 1;
}
})();

4
js/cli/src/rust.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*/Cargo.toml" {
const mod: any;
export default mod;
}

13
js/cli/tsconfig.json Normal file
View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["ES2019"],
"sourceMap": false,
"rootDir": "src",
"moduleResolution": "node",
"module": "ESNext",
"allowSyntheticDefaultImports": true,
"strict": false
},
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,6 @@
#!/usr/bin/env node
var fs = require('fs');
var path = "dist/cli.js";
var data = "#!/usr/bin/env node\n\n";
data += fs.readFileSync(path);
fs.writeFileSync(path, data);

5
js/lib/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
dist
node_modules
target
**/.local*
*.tgz

8
js/lib/.npmignore Normal file
View file

@ -0,0 +1,8 @@
src
target
util
**/.local*
*.tgz
tsconfig.json
rollup.config.js
yarn-error.log

3
js/lib/README.md Normal file
View file

@ -0,0 +1,3 @@
This is a JavaScript wrapper for the Taplo TOML library.
All the documentation and information is available on the [website](https://taplo.tamasfe.dev).

33
js/lib/package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "@taplo/lib",
"version": "0.4.0-alpha.2",
"description": "A TOML linter and formatter and utility library.",
"types": "dist/index.d.ts",
"scripts": {
"build": "yarn rollup --silent -c rollup.config.js",
"prepublish": "RELEASE=true yarn build"
},
"homepage": "https://taplo.tamasfe.dev",
"main": "dist/index.js",
"repository": "https://github.com/tamasfe/taplo",
"author": {
"name": "tamasfe",
"url": "https://github.com/tamasfe"
},
"license": "MIT",
"private": false,
"dependencies": {
"@taplo/core": "^0.1.0"
},
"devDependencies": {
"@types/node": "^17.0.41",
"@wasm-tool/rollup-plugin-rust": "^2.2.2",
"rollup": "^2.75.6",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.32.1",
"tslib": "^2.0.3",
"typescript": "^4.7.3",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0"
}
}

34
js/lib/rollup.config.js Normal file
View file

@ -0,0 +1,34 @@
import rust from "@wasm-tool/rollup-plugin-rust";
import typescript from "rollup-plugin-typescript2";
import { terser } from "rollup-plugin-terser";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import path from "path";
import process from "process";
export default {
input: {
index: "src/index.ts",
},
output: {
sourcemap: false,
name: "taplo",
format: "umd",
dir: "dist",
},
plugins: [
rust({
debug: process.env["RELEASE"] !== "true",
nodejs: true,
inlineWasm: true,
}),
commonjs(),
resolve({
jsnext: true,
preferBuiltins: true,
rootDir: path.join(process.cwd(), ".."),
}),
typescript(),
terser(),
],
};

241
js/lib/src/index.ts Normal file
View file

@ -0,0 +1,241 @@
import {
Config,
convertEnv,
Environment,
FormatterOptions,
prepareEnv,
} from "@taplo/core";
import loadTaplo from "../../../crates/taplo-wasm/Cargo.toml";
import { objectCamel } from "./util";
/**
* Options for the format function.
*/
export interface FormatOptions {
/**
* Options to pass to the formatter.
*/
options?: FormatterOptions;
/**
* Taplo configuration, this can be parsed
* from files like `taplo.toml` or provided manually.
*/
config?: Config;
}
/**
* Options for TOML Lint.
*/
export interface LintOptions {
/**
* Taplo configuration, this can be parsed
* from `.taplo.toml` or provided manually.
*/
config?: Config;
}
/**
* An lint error.
*/
export interface LintError {
/**
* A range within the TOML document if any.
*/
range?: Range;
/**
* The error message.
*/
error: string;
}
/**
* The object returned from the lint function.
*/
export interface LintResult {
/**
* Lint errors, if any.
*
* This includes syntax, semantic and schema errors as well.
*/
errors: Array<LintError>;
}
/**
* This class allows for usage of the library in a synchronous context
* after being asynchronously initialized once.
*
* It cannot be constructed with `new`, and instead must be
* created by calling `initialize`.
*
* Example usage:
*
* ```js
* import { Taplo } from "taplo";
*
* // Somewhere at the start of your app.
* const taplo = await Taplo.initialize();
* // ...
* // The other methods will not return promises.
* const formatted = taplo.format(tomlDocument);
* ```
*/
export class Taplo {
private static taplo: any | undefined;
private static initializing: boolean = false;
private constructor(private env: Environment) {
if (!Taplo.initializing) {
throw new Error(
`an instance of Taplo can only be created by calling the "initialize" static method`
);
}
}
public static async initialize(env?: Environment): Promise<Taplo> {
if (typeof Taplo.taplo === "undefined") {
Taplo.taplo = await loadTaplo();
}
Taplo.taplo.initialize();
const environment = env ?? browserEnvironment();
prepareEnv(environment);
Taplo.initializing = true;
const t = new Taplo(environment);
Taplo.initializing = false;
return t;
}
/**
* Lint a TOML document, this function returns
* both syntax and semantic (e.g. conflicting keys) errors.
*
* If a JSON schema is found in the config, the TOML document will be validated with it
* only if it is syntactically valid.
*
* Example usage:
*
* ```js
* const lintResult = await taplo.lint(tomlDocument, {
* config: { schema: { url: "https://example.com/my-schema.json" } },
* });
*
* if (lintResult.errors.length > 0) {
* throw new Error("the document is invalid");
* }
* ```
*
* @param toml TOML document.
* @param options Optional additional options.
*/
public async lint(toml: string, options?: LintOptions): Promise<LintResult> {
return await Taplo.taplo.lint(
convertEnv(this.env),
toml,
objectCamel(options?.config ?? {})
);
}
/**
* Format the given TOML document.
*
* @param toml TOML document.
* @param options Optional format options.
*/
public format(toml: string, options?: FormatOptions): string {
try {
return Taplo.taplo.format(
convertEnv(this.env),
toml,
options?.options ?? {},
objectCamel(options?.config ?? {})
);
} catch (e) {
throw new Error(e);
}
}
/**
* Encode the given JavaScript object to TOML.
*
* @throws If the given object cannot be serialized to TOML.
*
* @param data JSON compatible JavaScript object or JSON string.
*/
public encode(data: object | string): string {
if (typeof data !== "string") {
data = JSON.stringify(data);
}
try {
return Taplo.taplo.from_json(data);
} catch (e) {
throw new Error(e);
}
}
/**
* Decode the given TOML string to a JavaScript object.
*
* @throws If data is not valid TOML.
*
* @param {string} data TOML string.
*/
public decode<T extends object = any>(data: string): T;
/**
* Convert the given TOML string to JSON.
*
* @throws If data is not valid TOML.
*
* @param data TOML string.
* @param {boolean} parse Whether to keep the JSON in a string format.
*/
public decode(data: string, parse: false): string;
public decode<T extends object = any>(
data: string,
parse: boolean = true
): T | string {
let v: string;
try {
v = Taplo.taplo.to_json(data);
} catch (e) {
throw new Error(e);
}
if (parse) {
return JSON.parse(v);
} else {
return v;
}
}
}
/**
* A very limited default environment inside a browser.
*/
function browserEnvironment(): Environment {
return {
cwd: () => "",
envVar: () => "",
findConfigFile: () => undefined,
glob: () => [],
isAbsolute: () => true,
now: () => new Date(),
readFile: () => Promise.reject("not implemented"),
writeFile: () => Promise.reject("not implemented"),
stderr: async bytes => {
console.error(new TextDecoder().decode(bytes));
return bytes.length;
},
stdErrAtty: () => false,
stdin: () => Promise.reject("not implemented"),
stdout: async bytes => {
console.log(new TextDecoder().decode(bytes));
return bytes.length;
},
urlToFilePath: (url: string) => url.slice("file://".length),
};
}

4
js/lib/src/rust.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*/Cargo.toml" {
const mod: any;
export default mod;
}

34
js/lib/src/util.ts Normal file
View file

@ -0,0 +1,34 @@
const toCamel = (str: string): string => {
return str.replace(/([_-][a-z])/gi, ($1: string) => {
return $1.toUpperCase().replace("-", "").replace("_", "");
});
};
const isArray = function (
input: Record<string, unknown> | Record<string, unknown>[] | unknown
): input is Record<string, unknown>[] {
return Array.isArray(input);
};
const isObject = function (
obj: Record<string, unknown> | Record<string, unknown>[] | unknown
): obj is Record<string, unknown> {
return (
obj === Object(obj) && !Array.isArray(obj) && typeof obj !== "function"
);
};
export const objectCamel = function <T>(input: T): T {
return (function recurse<
K extends Record<string, unknown> | Record<string, unknown>[] | unknown
>(input: K): K {
if (isObject(input)) {
return Object.keys(input).reduce((acc, key) => {
return Object.assign(acc, { [toCamel(key)]: recurse(input[key]) });
}, {} as K);
} else if (isArray(input)) {
return input.map(i => recurse(i)) as K;
}
return input;
})(input);
};

14
js/lib/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["ES2019", "dom"],
"sourceMap": false,
"rootDir": "src",
"moduleResolution": "node",
"module": "ESNext",
"allowSyntheticDefaultImports": true,
"declaration": true,
"strict": false
},
"exclude": ["node_modules"]
}

6
js/lsp/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
dist
node_modules
target
**/.local*
*.tgz
yarn-error.log

8
js/lsp/.npmignore Normal file
View file

@ -0,0 +1,8 @@
src
target
util
**/.local*
*.tgz
tsconfig.json
rollup.config.js
yarn-error.log

3
js/lsp/README.md Normal file
View file

@ -0,0 +1,3 @@
This is a JavaScript wrapper for the Taplo TOML CLI tool.
All the documentation and information is available on the [website](https://taplo.tamasfe.dev).

32
js/lsp/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "@taplo/lsp",
"version": "0.3.0-alpha.4",
"description": "A JavaScript wrapper for the Taplo TOML language server.",
"scripts": {
"build": "yarn rollup --silent -c rollup.config.js",
"prepublish": "RELEASE=true yarn build"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://taplo.tamasfe.dev",
"repository": "https://github.com/tamasfe/taplo",
"author": {
"name": "tamasfe",
"url": "https://github.com/tamasfe"
},
"license": "MIT",
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@types/node": "^17.0.41",
"@wasm-tool/rollup-plugin-rust": "^2.2.2",
"rollup": "^2.75.6",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.32.1",
"tslib": "^2.4.0",
"typescript": "^4.7.3"
},
"dependencies": {
"@taplo/core": "^0.1.0"
}
}

35
js/lsp/rollup.config.js Normal file
View file

@ -0,0 +1,35 @@
import typescript from "rollup-plugin-typescript2";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { terser } from "rollup-plugin-terser";
import rust from "@wasm-tool/rollup-plugin-rust";
import path from "path";
import process from "process";
export default {
input: {
index: "src/index.ts",
},
output: {
sourcemap: false,
name: "taploLsp",
format: "umd",
dir: "dist",
},
plugins: [
rust({
debug: process.env["RELEASE"] !== "true",
nodejs: true,
inlineWasm: true,
cargoArgs: ["--features", "lsp"],
}),
commonjs(),
resolve({
jsnext: true,
preferBuiltins: true,
rootDir: path.join(process.cwd(), ".."),
}),
typescript(),
terser(),
],
};

62
js/lsp/src/index.ts Normal file
View file

@ -0,0 +1,62 @@
import loadTaplo from "../../../crates/taplo-wasm/Cargo.toml";
import { convertEnv, Environment, Lsp, prepareEnv } from "@taplo/core";
export interface RpcMessage {
jsonrpc: "2.0";
method?: string;
id?: string | number;
params?: any;
result?: any;
error?: any;
}
export interface LspInterface {
/**
* Handler for RPC messages set from the LSP server.
*/
onMessage: (message: RpcMessage) => void;
}
export class TaploLsp {
private static taplo: any | undefined;
private static initializing: boolean = false;
private constructor(private env: Environment, private lspInner: any) {
if (!TaploLsp.initializing) {
throw new Error(
`an instance of Taplo can only be created by calling the "initialize" static method`
);
}
}
public static async initialize(
env: Environment,
lspInterface: LspInterface
): Promise<TaploLsp> {
if (typeof TaploLsp.taplo === "undefined") {
TaploLsp.taplo = await loadTaplo();
}
TaploLsp.taplo.initialize();
prepareEnv(env);
TaploLsp.initializing = true;
const t = new TaploLsp(
env,
TaploLsp.taplo.create_lsp(convertEnv(env), {
js_on_message: lspInterface.onMessage,
})
);
TaploLsp.initializing = false;
return t;
}
public send(message: RpcMessage) {
this.lspInner.send(message);
}
public dispose() {
this.lspInner.free();
}
}

4
js/lsp/src/rust.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*/Cargo.toml" {
const mod: any;
export default mod;
}

14
js/lsp/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["ES2019"],
"sourceMap": false,
"rootDir": "src",
"moduleResolution": "node",
"module": "ESNext",
"declaration": true,
"allowSyntheticDefaultImports": true,
"strict": false
},
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,6 @@
#!/usr/bin/env node
var fs = require('fs');
var path = "dist/cli.js";
var data = "#!/usr/bin/env node\n\n";
data += fs.readFileSync(path);
fs.writeFileSync(path, data);

9
js/package.json Normal file
View file

@ -0,0 +1,9 @@
{
"private": true,
"workspaces": [
"cli",
"core",
"lib",
"lsp"
]
}

2032
js/yarn.lock Normal file

File diff suppressed because it is too large Load diff