diff --git a/.cargo/config.toml b/.cargo/config.toml
deleted file mode 100644
index 7624b335..00000000
--- a/.cargo/config.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-[alias]
-snap = "insta test"
-xtask = "run --package xtask --"
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..43994f87
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "postgrestools.bin": "./target/debug/postgrestools"
+}
diff --git a/Cargo.lock b/Cargo.lock
index 8844e46b..032c60af 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1456,9 +1456,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
-version = "0.4.15"
+version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
@@ -2412,16 +2412,6 @@ dependencies = [
"tracing-tree",
]
-[[package]]
-name = "pgt_commands"
-version = "0.0.0"
-dependencies = [
- "anyhow",
- "async-std",
- "pgt_text_size",
- "sqlx",
-]
-
[[package]]
name = "pgt_completions"
version = "0.0.0"
@@ -2578,6 +2568,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
+ "strum",
"tokio",
"tower",
"tower-lsp",
@@ -2749,6 +2740,7 @@ dependencies = [
"biome_rowan",
"dashmap 5.5.3",
"futures",
+ "globset",
"ignore",
"pgt_analyse",
"pgt_analyser",
@@ -2767,6 +2759,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
+ "strum",
"tempfile",
"tokio",
"tracing",
@@ -3309,6 +3302,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
[[package]]
name = "ryu"
version = "1.0.18"
@@ -3816,6 +3815,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "strum"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.90",
+]
+
[[package]]
name = "subtle"
version = "2.6.1"
diff --git a/Cargo.toml b/Cargo.toml
index dc175660..3fcbf7f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ serde = "1.0.195"
serde_json = "1.0.114"
similar = "2.6.0"
smallvec = { version = "1.13.2", features = ["union", "const_new", "serde"] }
+strum = { version = "0.27.1", features = ["derive"] }
# this will use tokio if available, otherwise async-std
sqlx = { version = "0.8.2", features = ["runtime-tokio", "runtime-async-std", "postgres", "json"] }
syn = "1.0.109"
@@ -56,7 +57,6 @@ pgt_analyse = { path = "./crates/pgt_analyse", version = "0.0.0"
pgt_analyser = { path = "./crates/pgt_analyser", version = "0.0.0" }
pgt_base_db = { path = "./crates/pgt_base_db", version = "0.0.0" }
pgt_cli = { path = "./crates/pgt_cli", version = "0.0.0" }
-pgt_commands = { path = "./crates/pgt_commands", version = "0.0.0" }
pgt_completions = { path = "./crates/pgt_completions", version = "0.0.0" }
pgt_configuration = { path = "./crates/pgt_configuration", version = "0.0.0" }
pgt_console = { path = "./crates/pgt_console", version = "0.0.0" }
diff --git a/crates/pgt_commands/Cargo.toml b/crates/pgt_commands/Cargo.toml
deleted file mode 100644
index 31b6d589..00000000
--- a/crates/pgt_commands/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-authors.workspace = true
-categories.workspace = true
-description = ""
-edition.workspace = true
-homepage.workspace = true
-keywords.workspace = true
-license.workspace = true
-name = "pgt_commands"
-repository.workspace = true
-version = "0.0.0"
-
-
-[dependencies]
-anyhow = "1.0.62"
-async-std = "1.12.0"
-pgt_text_size.workspace = true
-sqlx.workspace = true
-
-[lib]
-doctest = false
diff --git a/crates/pgt_commands/src/command.rs b/crates/pgt_commands/src/command.rs
deleted file mode 100644
index 9449efb6..00000000
--- a/crates/pgt_commands/src/command.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-pub enum CommandType {
- ExecuteStatement,
-}
-
-impl CommandType {
- pub const ALL: [CommandType; 1] = [CommandType::ExecuteStatement];
-
- pub fn id(&self) -> &str {
- match self {
- CommandType::ExecuteStatement => "executeStatement",
- }
- }
-
- pub fn label(&self) -> &str {
- match self {
- CommandType::ExecuteStatement => "Execute Statement",
- }
- }
-
- pub fn from_id(s: &str) -> Option {
- match s {
- "executeStatement" => Some(CommandType::ExecuteStatement),
- _ => None,
- }
- }
-}
-
-pub trait Command {
- type ExecuteStatement;
-
- fn command_type() -> CommandType;
-}
diff --git a/crates/pgt_commands/src/execute_statement.rs b/crates/pgt_commands/src/execute_statement.rs
deleted file mode 100644
index 299f9d36..00000000
--- a/crates/pgt_commands/src/execute_statement.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use sqlx::{Executor, PgPool, postgres::PgQueryResult};
-
-use crate::command::{Command, CommandType};
-
-pub struct ExecuteStatementCommand {
- statement: String,
-}
-
-impl ExecuteStatementCommand {
- pub fn new(statement: String) -> Self {
- Self { statement }
- }
-
- pub async fn run(&self, conn: Option) -> anyhow::Result {
- match conn {
- Some(conn) => match conn.execute(self.statement.as_str()).await {
- Ok(res) => Ok(res),
- Err(e) => Err(anyhow::anyhow!(e.to_string())),
- },
- _ => Err(anyhow::anyhow!("No connection to database".to_string())),
- }
- }
-
- pub fn trim_statement(stmt: String, max_length: usize) -> String {
- let len = stmt.len();
- if len <= max_length {
- return stmt;
- }
-
- let half = max_length / 2;
- let start = &stmt[..half];
- let end = &stmt[len - half + (max_length % 2)..];
-
- format!("{}...{}", start, end)
- }
-}
-
-impl Command for ExecuteStatementCommand {
- type ExecuteStatement = ExecuteStatementCommand;
-
- fn command_type() -> CommandType {
- CommandType::ExecuteStatement
- }
-}
diff --git a/crates/pgt_commands/src/lib.rs b/crates/pgt_commands/src/lib.rs
deleted file mode 100644
index 8a58fb11..00000000
--- a/crates/pgt_commands/src/lib.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod command;
-pub mod execute_statement;
-
-pub use command::*;
-pub use execute_statement::*;
diff --git a/crates/pgt_configuration/src/database.rs b/crates/pgt_configuration/src/database.rs
index 6ec5773f..209f86dc 100644
--- a/crates/pgt_configuration/src/database.rs
+++ b/crates/pgt_configuration/src/database.rs
@@ -1,3 +1,4 @@
+use biome_deserialize::StringSet;
use biome_deserialize_macros::{Merge, Partial};
use bpaf::Bpaf;
use serde::{Deserialize, Serialize};
@@ -28,6 +29,9 @@ pub struct DatabaseConfiguration {
#[partial(bpaf(long("database")))]
pub database: String,
+ #[partial(bpaf(long("allow_statement_executions_against")))]
+ pub allow_statement_executions_against: StringSet,
+
/// The connection timeout in seconds.
#[partial(bpaf(long("conn_timeout_secs"), fallback(Some(10)), debug_fallback))]
pub conn_timeout_secs: u16,
@@ -41,6 +45,7 @@ impl Default for DatabaseConfiguration {
username: "postgres".to_string(),
password: "postgres".to_string(),
database: "postgres".to_string(),
+ allow_statement_executions_against: Default::default(),
conn_timeout_secs: 10,
}
}
diff --git a/crates/pgt_configuration/src/lib.rs b/crates/pgt_configuration/src/lib.rs
index aaf5ac07..f262450d 100644
--- a/crates/pgt_configuration/src/lib.rs
+++ b/crates/pgt_configuration/src/lib.rs
@@ -111,6 +111,7 @@ impl PartialConfiguration {
password: Some("postgres".to_string()),
database: Some("postgres".to_string()),
conn_timeout_secs: Some(10),
+ allow_statement_executions_against: Default::default(),
}),
}
}
diff --git a/crates/pgt_lsp/Cargo.toml b/crates/pgt_lsp/Cargo.toml
index 56086da0..4fafc27f 100644
--- a/crates/pgt_lsp/Cargo.toml
+++ b/crates/pgt_lsp/Cargo.toml
@@ -28,6 +28,7 @@ pgt_workspace = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
+strum = { workspace = true }
tokio = { workspace = true, features = ["rt", "io-std"] }
tower-lsp = { version = "0.20.0" }
tracing = { workspace = true, features = ["attributes"] }
diff --git a/crates/pgt_lsp/src/capabilities.rs b/crates/pgt_lsp/src/capabilities.rs
index 8258811e..6bc81084 100644
--- a/crates/pgt_lsp/src/capabilities.rs
+++ b/crates/pgt_lsp/src/capabilities.rs
@@ -1,10 +1,15 @@
use pgt_lsp_converters::{PositionEncoding, WideEncoding, negotiated_encoding};
+use pgt_workspace::code_actions::{CommandActionCategory, CommandActionCategoryIter};
+use strum::{EnumIter, IntoEnumIterator};
use tower_lsp::lsp_types::{
- ClientCapabilities, CompletionOptions, PositionEncodingKind, SaveOptions, ServerCapabilities,
- TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
- TextDocumentSyncSaveOptions, WorkDoneProgressOptions,
+ ClientCapabilities, CodeActionOptions, CompletionOptions, ExecuteCommandOptions,
+ PositionEncodingKind, SaveOptions, ServerCapabilities, TextDocumentSyncCapability,
+ TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
+ WorkDoneProgressOptions,
};
+use crate::handlers::code_actions::command_id;
+
/// The capabilities to send from server as part of [`InitializeResult`]
///
/// [`InitializeResult`]: lspower::lsp::InitializeResult
@@ -46,10 +51,19 @@ pub(crate) fn server_capabilities(capabilities: &ClientCapabilities) -> ServerCa
work_done_progress: None,
},
}),
+ execute_command_provider: Some(ExecuteCommandOptions {
+ commands: CommandActionCategory::iter()
+ .map(|c| command_id(&c))
+ .collect::>(),
+
+ ..Default::default()
+ }),
document_formatting_provider: None,
document_range_formatting_provider: None,
document_on_type_formatting_provider: None,
- code_action_provider: None,
+ code_action_provider: Some(tower_lsp::lsp_types::CodeActionProviderCapability::Simple(
+ true,
+ )),
rename_provider: None,
..Default::default()
}
diff --git a/crates/pgt_lsp/src/handlers.rs b/crates/pgt_lsp/src/handlers.rs
index d6a8e28f..638a6d03 100644
--- a/crates/pgt_lsp/src/handlers.rs
+++ b/crates/pgt_lsp/src/handlers.rs
@@ -1,2 +1,4 @@
+pub(crate) mod code_actions;
pub(crate) mod completions;
+mod helper;
pub(crate) mod text_document;
diff --git a/crates/pgt_lsp/src/handlers/code_actions.rs b/crates/pgt_lsp/src/handlers/code_actions.rs
new file mode 100644
index 00000000..140eb8a3
--- /dev/null
+++ b/crates/pgt_lsp/src/handlers/code_actions.rs
@@ -0,0 +1,114 @@
+use crate::session::Session;
+use anyhow::{Result, anyhow};
+use tower_lsp::lsp_types::{
+ self, CodeAction, CodeActionDisabled, CodeActionOrCommand, Command, ExecuteCommandParams,
+ MessageType,
+};
+
+use pgt_workspace::code_actions::{
+ CodeActionKind, CodeActionsParams, CommandActionCategory, ExecuteStatementParams,
+};
+
+use super::helper;
+
+pub fn get_actions(
+ session: &Session,
+ params: lsp_types::CodeActionParams,
+) -> Result {
+ let url = params.text_document.uri;
+ let path = session.file_path(&url)?;
+
+ let cursor_position = helper::get_cursor_position(session, &url, params.range.start)?;
+
+ let workspace_actions = session.workspace.pull_code_actions(CodeActionsParams {
+ path,
+ cursor_position,
+ only: vec![],
+ skip: vec![],
+ })?;
+
+ let actions: Vec = workspace_actions
+ .actions
+ .into_iter()
+ .filter_map(|action| match action.kind {
+ CodeActionKind::Command(command) => {
+ let command_id: String = command_id(&command.category);
+ let title = action.title;
+
+ match command.category {
+ CommandActionCategory::ExecuteStatement(stmt_id) => Some(CodeAction {
+ title: title.clone(),
+ kind: Some(lsp_types::CodeActionKind::EMPTY),
+ command: Some({
+ Command {
+ title: title.clone(),
+ command: command_id,
+ arguments: Some(vec![
+ serde_json::Value::Number(stmt_id.into()),
+ serde_json::to_value(&url).unwrap(),
+ ]),
+ }
+ }),
+ disabled: action
+ .disabled_reason
+ .map(|reason| CodeActionDisabled { reason }),
+ ..Default::default()
+ }),
+ }
+ }
+
+ _ => todo!(),
+ })
+ .collect();
+
+ Ok(actions
+ .into_iter()
+ .map(|ac| CodeActionOrCommand::CodeAction(ac))
+ .collect())
+}
+
+pub fn command_id(command: &CommandActionCategory) -> String {
+ match command {
+ CommandActionCategory::ExecuteStatement(_) => "pgt.executeStatement".into(),
+ }
+}
+
+pub async fn execute_command(
+ session: &Session,
+ params: ExecuteCommandParams,
+) -> anyhow::Result