feat: add LSP server, VSCode + DataGrip extensions, release infra, autofix
- pkg/lsp: JSON-RPC 2.0 LSP server (formatting, diagnostics, codeAction quick-fixes) - cmd/pgtidy: lsp and config subcommands - pkg/diagnostics: TextFix struct for byte-range autofixes - pkg/lint: MIG001/MIG003 autofixes, ApplyFixes helper, --fix flag on lint command - editors/vscode: TypeScript extension with LanguageClient, showVersion/showConfig/formatDocument commands, logo - editors/datagrip: Gradle JetBrains plugin via LSP4IJ, pluginIcon - .goreleaser.yaml, .github/workflows: CI + release pipeline - Makefile: snapshot, release, vscode-compile, vscode-package targets - go.mod + all imports: module path updated to git.warky.dev/wdevs/pgtidy - assets: logo files (256px, 128px, 1024px, ico)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.gradle/
|
||||
build/
|
||||
*.zip
|
||||
@@ -0,0 +1,37 @@
|
||||
# PgTidy — DataGrip / JetBrains Plugin
|
||||
|
||||
Formats and lints SQL files in DataGrip (and any JetBrains IDE) via the LSP4IJ plugin.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `pgtidy` binary on `PATH` — download from [releases](https://git.warky.dev/wdevs/PgTidy/releases) or build with `go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest`
|
||||
- [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin installed (free, by Red Hat)
|
||||
- DataGrip 2024.3+ (or any JetBrains IDE 2024.3+)
|
||||
|
||||
## Install the plugin
|
||||
|
||||
### Option A — Install from disk (`.zip`)
|
||||
|
||||
1. Build: `./gradlew buildPlugin` (output in `build/distributions/`)
|
||||
2. In DataGrip: **Settings → Plugins → ⚙ → Install Plugin from Disk…** → select the `.zip`
|
||||
3. Restart the IDE
|
||||
|
||||
### Option B — Install LSP4IJ and configure manually (no plugin build needed)
|
||||
|
||||
1. Install **LSP4IJ** from the marketplace (**Settings → Plugins → Marketplace → search "LSP4IJ"**)
|
||||
2. Go to **Settings → Language Servers → + (Add)**
|
||||
3. Fill in:
|
||||
- **Name:** `PgTidy`
|
||||
- **Command:** `pgtidy lsp`
|
||||
4. Under **Mappings**, add file patterns: `*.sql`, `*.pgsql`
|
||||
5. Click **OK** and restart the IDE
|
||||
|
||||
## Features
|
||||
|
||||
- **Formatting** — `Code → Reformat Code` (`Ctrl+Alt+L`) formats the current SQL file
|
||||
- **Diagnostics** — lint findings shown as inspections (MIG001–MIG003, COR001–003, NAM001–003)
|
||||
- **Quick fixes** — intention actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Server not starting:** Check **View → Tool Windows → LSP4IJ Consoles** for stderr output. Most common cause: `pgtidy` not found on `PATH` — update the command to the full path, e.g. `/usr/local/bin/pgtidy lsp`.
|
||||
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id("org.jetbrains.intellij.platform") version "2.3.0"
|
||||
kotlin("jvm") version "2.0.0"
|
||||
}
|
||||
|
||||
group = providers.gradleProperty("pluginGroup").get()
|
||||
version = providers.gradleProperty("pluginVersion").get()
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
intellijPlatform {
|
||||
defaultRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
intellijPlatform {
|
||||
val platformVersion = providers.gradleProperty("platformVersion")
|
||||
val platformType = providers.gradleProperty("platformType")
|
||||
create(platformType, platformVersion)
|
||||
bundledPlugin("com.intellij.database")
|
||||
plugin("com.redhat.devtools.lsp4ij:${providers.gradleProperty("lsp4ijVersion").get()}")
|
||||
instrumentationTools()
|
||||
}
|
||||
}
|
||||
|
||||
intellijPlatform {
|
||||
pluginConfiguration {
|
||||
name = providers.gradleProperty("pluginName")
|
||||
version = providers.gradleProperty("pluginVersion")
|
||||
ideaVersion {
|
||||
sinceBuild = providers.gradleProperty("pluginSinceBuild")
|
||||
}
|
||||
}
|
||||
signing {
|
||||
// Set CERTIFICATE_CHAIN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD env vars for release signing.
|
||||
certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN")
|
||||
privateKey = providers.environmentVariable("PRIVATE_KEY")
|
||||
privateKeyPassword = providers.environmentVariable("PRIVATE_KEY_PASSWORD")
|
||||
}
|
||||
publishing {
|
||||
token = providers.environmentVariable("PUBLISH_TOKEN")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pluginGroup=com.pgtidy
|
||||
pluginName=PgTidy
|
||||
pluginVersion=0.1.0
|
||||
pluginSinceBuild=243
|
||||
# DataGrip 2024.3
|
||||
platformVersion=2024.3
|
||||
platformType=DB
|
||||
lsp4ijVersion=0.8.0
|
||||
@@ -0,0 +1,5 @@
|
||||
rootProject.name = "pgtidy-datagrip"
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.pgtidy.datagrip
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider
|
||||
|
||||
class PgTidyServerConnection(project: Project) : ProcessStreamConnectionProvider(
|
||||
listOf("pgtidy", "lsp"),
|
||||
project.basePath ?: System.getProperty("user.home"),
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.pgtidy.datagrip
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.redhat.devtools.lsp4ij.LanguageServerFactory
|
||||
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
|
||||
|
||||
class PgTidyServerFactory : LanguageServerFactory {
|
||||
override fun createConnectionProvider(project: Project): StreamConnectionProvider =
|
||||
PgTidyServerConnection(project)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<idea-plugin>
|
||||
<id>com.pgtidy.datagrip</id>
|
||||
<name>PgTidy</name>
|
||||
<version>0.1.0</version>
|
||||
<vendor url="https://git.warky.dev/wdevs/PgTidy">Warky Devs</vendor>
|
||||
<description><![CDATA[
|
||||
PostgreSQL formatter and linter powered by <a href="https://github.com/hein/pgtidy">pgtidy</a>.<br/>
|
||||
Requires the <code>pgtidy</code> binary on PATH and the
|
||||
<a href="https://plugins.jetbrains.com/plugin/23257-lsp4ij">LSP4IJ</a> plugin.
|
||||
]]></description>
|
||||
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<depends>com.intellij.database</depends>
|
||||
<depends>com.redhat.devtools.lsp4ij</depends>
|
||||
|
||||
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
|
||||
<server id="com.pgtidy.lsp"
|
||||
name="PgTidy"
|
||||
factoryClass="com.pgtidy.datagrip.PgTidyServerFactory">
|
||||
<description>PostgreSQL formatter and linter (pgtidy lsp)</description>
|
||||
</server>
|
||||
|
||||
<fileNamePatternMapping patterns="*.sql;*.pgsql"
|
||||
serverId="com.pgtidy.lsp"
|
||||
languageId="SQL"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,4 @@
|
||||
out/
|
||||
node_modules/
|
||||
*.vsix
|
||||
assets/
|
||||
@@ -0,0 +1,10 @@
|
||||
.vscode/**
|
||||
src/**
|
||||
node_modules/**
|
||||
.gitignore
|
||||
.gitattributes
|
||||
tsconfig.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
**/*.ts
|
||||
**/*.map
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../LICENSE
|
||||
@@ -0,0 +1,75 @@
|
||||
# PgTidy — VSCode Extension
|
||||
|
||||
Formats and lints PostgreSQL/PL-pgSQL files using the `pgtidy` language server.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `pgtidy` binary on `PATH` (see [Build the binary](#build-the-binary) below)
|
||||
- Go 1.21+ (to build from source)
|
||||
- VSCode 1.85+
|
||||
|
||||
## Build the binary
|
||||
|
||||
**Option A — install directly with Go (recommended):**
|
||||
|
||||
```sh
|
||||
go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest
|
||||
```
|
||||
|
||||
The binary lands in `$(go env GOPATH)/bin/`. Make sure that directory is on your `PATH`.
|
||||
|
||||
**Option B — build from source and place manually:**
|
||||
|
||||
```sh
|
||||
git clone https://git.warky.dev/wdevs/PgTidy
|
||||
cd PgTidy
|
||||
go build -o pgtidy ./cmd/pgtidy
|
||||
|
||||
# Linux / macOS — move to a directory on PATH
|
||||
mv pgtidy /usr/local/bin/
|
||||
|
||||
# Windows — move to a directory on PATH, e.g.
|
||||
# move pgtidy.exe C:\tools\
|
||||
```
|
||||
|
||||
Verify the binary is reachable:
|
||||
|
||||
```sh
|
||||
pgtidy version
|
||||
```
|
||||
|
||||
If you place it somewhere not on `PATH`, set the full path in VSCode settings:
|
||||
|
||||
```json
|
||||
"pgtidy.path": "/path/to/pgtidy"
|
||||
```
|
||||
|
||||
## Install from `.vsix`
|
||||
|
||||
1. Build the package: `pnpm run build && pnpm run package`
|
||||
2. In VSCode: **Extensions** → `···` menu → **Install from VSIX…** → select `pgtidy-*.vsix`
|
||||
|
||||
Or via CLI:
|
||||
|
||||
```sh
|
||||
code --install-extension pgtidy-0.1.0.vsix
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `pgtidy.path` | `"pgtidy"` | Path to the binary if not on `PATH` |
|
||||
| `pgtidy.enable` | `true` | Enable/disable the language server |
|
||||
|
||||
## Features
|
||||
|
||||
- **Format on save** — configure via VSCode's `editor.formatOnSave`
|
||||
- **Diagnostics** — lint findings shown inline as you type (MIG001–MIG003, COR001–003, NAM001–003)
|
||||
- **Quick fixes** — lightbulb actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`)
|
||||
|
||||
`.sql` and `.pgsql` files are both handled.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No formatting / diagnostics:** Open the Output panel → select **PgTidy** to see server errors. Most common cause: `pgtidy` not found — set `pgtidy.path` to the full binary path.
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "pgtidy",
|
||||
"displayName": "PgTidy",
|
||||
"description": "PostgreSQL formatter and linter powered by pgtidy",
|
||||
"version": "0.1.0",
|
||||
"publisher": "warky-devs",
|
||||
"author": {
|
||||
"name": "Hein Puth",
|
||||
"email": "hein.puth@gmail.com"
|
||||
},
|
||||
"icon": "assets/logo_256.png",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.warky.dev/wdevs/PgTidy"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
},
|
||||
"categories": [
|
||||
"Formatters",
|
||||
"Linters"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onCommand:pgtidy.showVersion",
|
||||
"onCommand:pgtidy.showConfig",
|
||||
"onCommand:pgtidy.formatDocument"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "pgtidy.showVersion",
|
||||
"title": "Show Version",
|
||||
"category": "PgTidy"
|
||||
},
|
||||
{
|
||||
"command": "pgtidy.showConfig",
|
||||
"title": "Show Config",
|
||||
"category": "PgTidy"
|
||||
},
|
||||
{
|
||||
"command": "pgtidy.formatDocument",
|
||||
"title": "Format Document",
|
||||
"category": "PgTidy"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "sql",
|
||||
"extensions": [
|
||||
".pgsql"
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "PgTidy",
|
||||
"properties": {
|
||||
"pgtidy.path": {
|
||||
"type": "string",
|
||||
"default": "pgtidy",
|
||||
"description": "Path to the pgtidy binary. Defaults to 'pgtidy' (must be on PATH)."
|
||||
},
|
||||
"pgtidy.enable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable the PgTidy language server."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p ./",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"package": "vsce package --no-dependencies",
|
||||
"lint": "eslint src --ext ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/vscode": "^1.85.0",
|
||||
"@vscode/vsce": "^2.24.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
Generated
+1589
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
allowBuilds:
|
||||
'@vscode/vsce-sign': true
|
||||
keytar: true
|
||||
@@ -0,0 +1,105 @@
|
||||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
TransportKind,
|
||||
} from 'vscode-languageclient/node';
|
||||
|
||||
let client: LanguageClient | undefined;
|
||||
let outputChannel: vscode.OutputChannel;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
outputChannel = vscode.window.createOutputChannel('PgTidy');
|
||||
context.subscriptions.push(outputChannel);
|
||||
|
||||
// Commands are always registered so they work even when the LSP is disabled.
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('pgtidy.showVersion', cmdShowVersion),
|
||||
vscode.commands.registerCommand('pgtidy.showConfig', cmdShowConfig),
|
||||
vscode.commands.registerCommand('pgtidy.formatDocument', cmdFormatDocument),
|
||||
);
|
||||
|
||||
const cfg = vscode.workspace.getConfiguration('pgtidy');
|
||||
if (!cfg.get<boolean>('enable', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bin = cfg.get<string>('path', 'pgtidy');
|
||||
|
||||
const serverOptions: ServerOptions = {
|
||||
command: bin,
|
||||
args: ['lsp'],
|
||||
transport: TransportKind.stdio,
|
||||
};
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ scheme: 'file', language: 'sql' }],
|
||||
synchronize: {
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{sql,pgsql}'),
|
||||
},
|
||||
};
|
||||
|
||||
client = new LanguageClient('pgtidy', 'PgTidy', serverOptions, clientOptions);
|
||||
context.subscriptions.push(client);
|
||||
client.start();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
return client?.stop();
|
||||
}
|
||||
|
||||
function binary(): string {
|
||||
return vscode.workspace.getConfiguration('pgtidy').get<string>('path', 'pgtidy');
|
||||
}
|
||||
|
||||
function contextDir(): string | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
return folders[0].uri.fsPath;
|
||||
}
|
||||
const doc = vscode.window.activeTextEditor?.document;
|
||||
if (doc && !doc.isUntitled) {
|
||||
return path.dirname(doc.uri.fsPath);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function cmdShowVersion(): void {
|
||||
cp.execFile(binary(), ['version'], (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
vscode.window.showErrorMessage(`PgTidy: ${stderr.trim() || err.message}`);
|
||||
return;
|
||||
}
|
||||
vscode.window.showInformationMessage(stdout.trim());
|
||||
});
|
||||
}
|
||||
|
||||
function cmdShowConfig(): void {
|
||||
const cwd = contextDir();
|
||||
cp.execFile(binary(), ['config'], { cwd }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
vscode.window.showErrorMessage(`PgTidy: ${stderr.trim() || err.message}`);
|
||||
return;
|
||||
}
|
||||
outputChannel.clear();
|
||||
outputChannel.appendLine('PgTidy — effective configuration');
|
||||
if (cwd) {
|
||||
outputChannel.appendLine(`(resolved from: ${cwd})`);
|
||||
}
|
||||
outputChannel.appendLine('');
|
||||
outputChannel.append(stdout);
|
||||
outputChannel.show(true);
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdFormatDocument(): Promise<void> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('PgTidy: no active editor');
|
||||
return;
|
||||
}
|
||||
await vscode.commands.executeCommand('editor.action.formatDocument');
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"outDir": "out",
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
Reference in New Issue
Block a user