diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e2904..1f5fe5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.7 + +- Consolidated developer documentation into `docs/DEVELOPMENT.md` and `docs/SQL_IMPLEMENTATION.md`. +- Added **SQLovely: Format SQL Files in Directory** to recursively format `.sql` files in a selected directory. +- Directory formatting runs without SQLovely Extras by default for safer batch formatting. +- Documented directory SQL formatting command usage. + ## 0.1.6 - Changed generated metadata headers to the compact pre-`BEGIN` `-- METADATA` layout. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fe420f..d54343c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Contributing -Use the standard validation commands before opening a pull request: +Thank you for considering a contribution to SQLovely. + +## Before opening a pull request + +Run the standard validation commands: ```bash npm ci @@ -9,4 +13,16 @@ npm test npm run package:vsix ``` -Keep formatter behavior conservative and add regression tests for every behavior change. +## Guidelines + +- Keep formatter behavior conservative. +- Add regression tests for behavior changes. +- Keep `README.md` user-facing. +- Put maintainer/developer details in `docs/`. +- Update `CHANGELOG.md` for user-visible changes. + +For development setup and implementation notes, see: + +- `docs/DEVELOPMENT.md` +- `docs/SQL_IMPLEMENTATION.md` +- `PACKAGING.md` diff --git a/README.md b/README.md index 55020c9..dbcb229 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,23 @@ # SQLovely -SQLovely is a VS Code extension for `.sql` files. It provides SQL highlighting, conservative formatting, metadata-header extras, diagnostics and quick fixes. +SQLovely adds practical SQL language support for `.sql` files in VS Code. -Watcom SQL is the default dialect. MSSQL support is available as a small secondary dialect surface, not as a complete T-SQL implementation. +It is focused on Watcom SQL by default and also includes a small MSSQL-oriented dialect surface for projects that need basic SQL Server-style support. -## Install locally +## Features -Build the package: +- SQL syntax highlighting for regular `.sql` files +- Conservative SQL formatting +- Optional metadata-header extras for procedures, functions and triggers +- Diagnostics and quick fixes for supported SQLovely rules +- Watcom SQL as the default dialect +- Basic MSSQL support for common T-SQL-style files -```bash -npm install -npm run package:vsix -``` - -Install the resulting VSIX: +## Quick start -```bash -code --install-extension out/sqlovely-*.vsix -``` +Open a `.sql` file and run commands from the VS Code Command Palette. -## Recommended workspace settings +Recommended workspace setting: ```json { @@ -30,7 +28,7 @@ code --install-extension out/sqlovely-*.vsix } ``` -To let VS Code format SQL files automatically on save, enable format-on-save for the SQL language: +To format SQL files automatically when saving: ```json { @@ -41,97 +39,44 @@ To let VS Code format SQL files automatically on save, enable format-on-save for } ``` -SQLovely Extras are applied during SQLovely formatting by default. This lets **Format Document** and VS Code format-on-save also keep supported metadata headers up to date. Disable it if formatting should only touch whitespace, indentation and keyword casing: - -```json -{ - "sqlovely.extras.applyWithFormatting": true -} -``` - -Extras can also run as a separate save participant. This is disabled by default because it can update files even when formatting is not requested: - -```json -{ - "sqlovely.extras.applyOnSave": false -} -``` - ## Commands - **SQLovely: Show Active Dialect** - **SQLovely: Switch Dialect** - **SQLovely: Format Current SQL File** +- **SQLovely: Format SQL Files in Directory** - **SQLovely: Insert or Update Metadata Header** - **SQLovely: Apply SQLovely Extras** -## Dialects - -The active dialect is configured with: - -```json -{ - "sqlovely.dialect": "watcom" -} -``` +## Formatting -Supported values: +SQLovely formats conservatively. It keeps SQL structure intact and focuses on readable, predictable cleanup: -- `watcom`: default dialect -- `mssql`: small secondary dialect surface - -The TextMate grammar is a broad SQLovely grammar for `.sql` files. Dialect-specific behavior lives in the formatter, object detection, SQLovely Extras, diagnostics and quick fixes. - -## Syntax highlighting - -The grammar covers the SQL structures SQLovely needs for regular SQL work: - -- `--`, `//` and `/* ... */` comments -- single-quoted strings with doubled apostrophe escaping -- binary/hex literals -- bracketed, double-quoted and backtick identifiers -- host variables, local variables, system variables and positional parameters -- numeric literals -- common SQL, Watcom SQL and SQL Server data types -- common built-in scalar, aggregate, date/time, XML and window functions -- DDL, DML, transaction and control-flow keywords -- procedure, function and trigger declarations -- common schema-object declarations -- `GO` batch separators for MSSQL-oriented files +- keyword and function casing +- basic block indentation +- trailing whitespace removal +- limiting consecutive blank lines +- final newline handling -The grammar is designed for highlighting. It is not a full SQL parser. +The formatter does not rewrite queries, split statements, align joins or migrate SQL between dialects. -See `docs/SYNTAX_GRAMMAR.md` and `docs/SQL_COVERAGE.md` for the detailed coverage matrix. +### Format one file -## Formatter +Use **SQLovely: Format Current SQL File** or VS Code's built-in **Format Document** command. -The formatter is conservative and line-oriented. Its defaults follow a compact SQL style: upper-case keywords/functions, spaces for indentation, 2-space indentation, trimmed trailing whitespace and a final newline. It supports: +### Format a directory -- keyword/function casing -- basic Watcom block indentation -- simple `BEGIN` / `END`, `IF` / `ELSE` / `ENDIF`, `CASE` and MSSQL `TRY` / `CATCH` indentation -- trailing whitespace removal -- limiting consecutive blank lines -- optional final newline enforcement +Use **SQLovely: Format SQL Files in Directory** to format every `.sql` file inside a selected folder. -It does not rewrite query structure, split statements, align JOINs or convert between dialects. +The command opens a folder picker, searches recursively for `.sql` files, asks for confirmation and then formats each file. It skips common local/build folders such as `.git`, `node_modules`, `out` and `dist`. -Formatter settings: - -```json -{ - "sqlovely.format.enabled": true, - "sqlovely.format.keywordCase": "upper", - "sqlovely.format.indentSize": 2, - "sqlovely.format.insertSpaces": true, - "sqlovely.format.maxConsecutiveBlankLines": 1, - "sqlovely.format.ensureFinalNewline": true -} -``` +Directory formatting intentionally applies formatting only. SQLovely Extras are not applied during this command. ## SQLovely Extras -SQLovely Extras are optional features that can update SQL files beyond pure formatting. The current extra inserts or updates a metadata header for procedures, functions and triggers. +SQLovely Extras are optional file-updating features beyond pure formatting. + +The current extra inserts or updates a metadata header for procedures, functions and triggers: ```sql CREATE PROCEDURE dbo.my_proc() @@ -152,52 +97,70 @@ BEGIN END; ``` -The header is inserted directly before the first `BEGIN` line of the detected procedure, function or trigger. Existing older SQLovely headers are moved to this layout when they are updated. +The header is inserted directly before the first `BEGIN` line of the detected object. Repeated runs update the existing SQLovely block instead of duplicating it. -Repeated runs update the existing SQLovely block instead of duplicating it. Object detection ignores declarations inside comments and single-quoted strings. +Extras are applied during normal SQLovely formatting by default: -Extra settings: +```json +{ + "sqlovely.extras.applyWithFormatting": true +} +``` + +Disable this if formatting should only touch whitespace, indentation and keyword casing: ```json { - "sqlovely.extras.enabled": true, - "sqlovely.extras.applyWithFormatting": true, - "sqlovely.extras.applyOnSave": false, - "sqlovely.extras.metadataHeader.enabled": true + "sqlovely.extras.applyWithFormatting": false } ``` -## Diagnostics and quick fixes +Extras can also run as a separate save participant. This is disabled by default: -SQLovely warns when a supported procedure, function or trigger has no SQLovely metadata header. The quick fix inserts the same idempotent header used by the command. +```json +{ + "sqlovely.extras.applyOnSave": false +} +``` + +## Dialects -Diagnostics do not change files by themselves. In addition to missing metadata headers, SQLovely can show informational diagnostics for lines longer than the configured style limit. +The active dialect is configured with: ```json { - "sqlovely.diagnostics.maxLineLength.enabled": true, - "sqlovely.diagnostics.maxLineLength.limit": 120 + "sqlovely.dialect": "watcom" } ``` -## Documentation +Supported values: -- `docs/GETTING_STARTED.md` -- `docs/WORKSPACE_SETTINGS.md` -- `docs/SYNTAX_GRAMMAR.md` -- `docs/SQL_COVERAGE.md` -- `PACKAGING.md` +- `watcom`: default dialect +- `mssql`: small secondary dialect surface + +The syntax grammar is intentionally broad. Dialect-specific behavior is handled by formatting, object detection, extras, diagnostics and quick fixes. + +## Diagnostics -## Development +SQLovely can show diagnostics for supported rules, such as: -```bash -npm install -npm run check -npm test -npm run package:vsix +- missing metadata headers for procedures, functions and triggers +- SQL lines longer than the configured line-length limit + +Diagnostics do not modify files by themselves. Quick fixes can apply supported changes when selected. + +```json +{ + "sqlovely.diagnostics.enabled": true, + "sqlovely.diagnostics.maxLineLength.limit": 120 +} ``` -Run in an Extension Development Host with `F5` from VS Code. +## Documentation + +- `docs/DEVELOPMENT.md` +- `docs/SQL_IMPLEMENTATION.md` +- `PACKAGING.md` ## Current limits diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..2e6ed29 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,93 @@ +# Development + +This document contains the development workflow for SQLovely. + +For user-facing extension usage, see `README.md`. +For release packaging, see `PACKAGING.md`. + +## Setup + +Install dependencies: + +```bash +npm install +``` + +For a clean install from the lockfile: + +```bash +npm ci +``` + +## Validate + +Run TypeScript checks: + +```bash +npm run check +``` + +Run the test suite: + +```bash +npm test +``` + +Build a VSIX package: + +```bash +npm run package:vsix +``` + +The generated package is written to `out/`. + +## Extension Development Host + +Open the repository in VS Code and press `F5` to start an Extension Development Host. + +Useful smoke tests: + +- open a `.sql` file +- run **SQLovely: Show Active Dialect** +- run **SQLovely: Format Current SQL File** +- run **SQLovely: Format SQL Files in Directory** +- run **SQLovely: Insert or Update Metadata Header** +- verify diagnostics and quick fixes + +## Settings during development + +A practical development workspace setup is: + +```json +{ + "sqlovely.dialect": "watcom", + "sqlovely.format.enabled": true, + "sqlovely.format.keywordCase": "upper", + "sqlovely.format.indentSize": 2, + "sqlovely.format.insertSpaces": true, + "sqlovely.extras.applyWithFormatting": true, + "sqlovely.extras.applyOnSave": false, + "[sql]": { + "editor.defaultFormatter": "tim-mayer.sqlovely" + } +} +``` + +For MSSQL-oriented smoke tests: + +```json +{ + "sqlovely.dialect": "mssql", + "sqlovely.extras.applyWithFormatting": true, + "sqlovely.extras.applyOnSave": false, + "[sql]": { + "editor.defaultFormatter": "tim-mayer.sqlovely" + } +} +``` + +## Directory formatting behavior + +**SQLovely: Format SQL Files in Directory** uses the normal `sqlovely.format.*` settings. + +It intentionally does not apply SQLovely Extras. Keep this behavior conservative because the command can touch many files at once. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md deleted file mode 100644 index 0fe49a3..0000000 --- a/docs/GETTING_STARTED.md +++ /dev/null @@ -1,26 +0,0 @@ -# Getting started - -1. Open the SQLovely folder. -2. Run `npm install`. -3. Run `npm run package:vsix`. -4. Install the VSIX from `out/`. -5. Open a `.sql` file. -6. Confirm that the active dialect is `watcom` with **SQLovely: Show Active Dialect**. - -Recommended starting settings: - -```json -{ - "sqlovely.dialect": "watcom", - "sqlovely.extras.applyWithFormatting": true, - "[sql]": { - "editor.defaultFormatter": "tim-mayer.sqlovely" - } -} -``` - -SQLovely Extras are applied with SQLovely formatting by default. Set `sqlovely.extras.applyWithFormatting` to `false` if format operations should not update metadata headers. - -## Metadata headers - -SQLovely can insert or update a metadata header for procedures, functions and triggers. The header is placed directly before the first `BEGIN` line. diff --git a/docs/SQL_COVERAGE.md b/docs/SQL_COVERAGE.md deleted file mode 100644 index cc2b395..0000000 --- a/docs/SQL_COVERAGE.md +++ /dev/null @@ -1,37 +0,0 @@ -# SQL coverage matrix - -SQLovely focuses on lexical highlighting and conservative editing support. The table below describes the intended coverage after the syntax audit. - -| SQL area | Highlighting | Formatter v1 | Object detection / extras | -| --- | --- | --- | --- | -| Line comments `--`, `//` | yes | preserved | ignored during object detection | -| Block comments `/* ... */` | yes | preserved | ignored during object detection | -| Single-quoted strings | yes | preserved | ignored during object detection | -| Bracketed identifiers | yes | preserved | supported for MSSQL objects | -| Double-quoted identifiers | yes | preserved | supported for Watcom and MSSQL objects | -| Host variables `:name` and `?` | yes | preserved | not object-relevant | -| SQL Server variables `@name`, `@@name` | yes | preserved | supported for MSSQL surface | -| Numeric and hex literals | yes | preserved | not object-relevant | -| General DML | yes | conservative keyword casing | not object-relevant | -| General DDL | yes | conservative keyword casing | routines supported | -| Watcom routines | yes | basic indentation | procedure/function/trigger supported | -| Watcom control flow | yes | basic indentation | not object-relevant | -| Watcom handlers/exceptions | yes | keyword casing | not object-relevant | -| Transactions and savepoints | yes | keyword casing | not object-relevant | -| MSSQL batches with `GO` | yes | root-level keyword handling | not object-relevant | -| MSSQL routines | yes | rudimentary formatting | procedure/function/trigger supported | -| Metadata headers | comments highlighted | inserted/updated via extras | inserted before the routine `BEGIN` line | -| Line length | highlighted normally | not wrapped automatically | informational diagnostic, default limit 120 | -| Full SQL validation | no | no | no | -| Schema-aware IntelliSense | no | no | no | -| Dialect migration | no | no | no | - -## Notes - -A TextMate grammar highlights lexical patterns. It cannot guarantee that a SQL file is syntactically valid for a database engine. SQLovely therefore treats syntax highlighting, formatting, diagnostics and future semantic features as separate layers. - -## Extras and formatting - -`sqlovely.extras.applyWithFormatting` is enabled by default. When enabled, normal SQLovely formatting can also apply enabled extras such as metadata-header insertion or updates. Disable it to keep formatting limited to whitespace, indentation and keyword casing. - -Metadata headers are placed directly before the first `BEGIN` line of the detected procedure, function or trigger. diff --git a/docs/SQL_IMPLEMENTATION.md b/docs/SQL_IMPLEMENTATION.md new file mode 100644 index 0000000..fad877f --- /dev/null +++ b/docs/SQL_IMPLEMENTATION.md @@ -0,0 +1,80 @@ +# SQL implementation notes + +This document describes SQLovely's current SQL-related implementation boundaries for maintainers. + +SQLovely is built around a broad `.sql` TextMate grammar plus conservative, dialect-aware editing features. It is not a SQL parser and does not validate complete SQL programs. + +## Dialects + +The active dialect is configured with: + +```json +{ + "sqlovely.dialect": "watcom" +} +``` + +Supported values: + +- `watcom`: default dialect +- `mssql`: small secondary dialect surface for basic SQL Server-style workflows + +The grammar stays intentionally broad. Dialect-specific behavior lives in: + +- formatter rules +- object detection +- SQLovely Extras +- diagnostics +- quick fixes + +## Syntax grammar + +The grammar highlights useful lexical regions in `.sql` files. + +| Area | Coverage | +| --- | --- | +| Comments | `--`, `//`, `/* ... */` | +| Strings | single-quoted strings, `N'...'`, doubled apostrophe escaping, hex/binary literals | +| Identifiers | bracketed identifiers, double-quoted identifiers, backtick identifiers, qualified object names | +| Variables | `@local`, `@@system`, `:host`, `?`, `$1`-style numbered parameters | +| Literals | decimal numbers, scientific notation, hexadecimal numbers, common language constants | +| DDL | common create/alter/drop statements and schema-object declarations | +| DML | select/insert/update/delete/merge/upsert and common query clauses | +| Watcom control flow | `IF`, `THEN`, `ELSEIF`, `ENDIF`, `LOOP`, `LEAVE`, `SIGNAL`, `RESIGNAL`, handlers and exceptions | +| Routines | procedure, proc, function and trigger declarations | +| Transactions | commit, rollback, savepoints and transaction blocks | +| MSSQL surface | `GO`, `CREATE OR ALTER`, `PROC`, variables and basic TRY/CATCH keywords | + +## Coverage matrix + +| SQL area | Highlighting | Formatter v1 | Object detection / extras | +| --- | --- | --- | --- | +| Line comments `--`, `//` | yes | preserved | ignored during object detection | +| Block comments `/* ... */` | yes | preserved | ignored during object detection | +| Single-quoted strings | yes | preserved | ignored during object detection | +| Bracketed identifiers | yes | preserved | supported for MSSQL objects | +| Double-quoted identifiers | yes | preserved | supported for Watcom and MSSQL objects | +| Host variables `:name` and `?` | yes | preserved | not object-relevant | +| SQL Server variables `@name`, `@@name` | yes | preserved | supported for MSSQL surface | +| Numeric and hex literals | yes | preserved | not object-relevant | +| General DML | yes | conservative keyword casing | not object-relevant | +| General DDL | yes | conservative keyword casing | routines supported | +| Watcom routines | yes | basic indentation | procedure/function/trigger supported | +| Watcom control flow | yes | basic indentation | not object-relevant | +| Watcom handlers/exceptions | yes | keyword casing | not object-relevant | +| Transactions and savepoints | yes | keyword casing | not object-relevant | +| MSSQL batches with `GO` | yes | root-level keyword handling | not object-relevant | +| MSSQL routines | yes | rudimentary formatting | procedure/function/trigger supported | +| Metadata headers | comments highlighted | inserted/updated through extras | supported for one primary object per file | + +## Design boundaries + +SQLovely currently does not provide: + +- complete SQL parsing +- schema-aware analysis +- IntelliSense +- validation of nested SQL blocks +- dialect-exclusive error reporting +- automatic conversion between Watcom SQL and MSSQL +- multi-object metadata-header management within one file diff --git a/docs/SYNTAX_GRAMMAR.md b/docs/SYNTAX_GRAMMAR.md deleted file mode 100644 index 4ae8fcc..0000000 --- a/docs/SYNTAX_GRAMMAR.md +++ /dev/null @@ -1,31 +0,0 @@ -# SQLovely syntax grammar - -The grammar is a TextMate grammar for highlighting `.sql` files. It is broad enough for daily Watcom SQL work and small MSSQL migration tests, but it is not a parser. - -## Covered lexical areas - -| Area | Coverage | -| --- | --- | -| Comments | `--`, `//`, `/* ... */` | -| Strings | single-quoted strings, `N'...'`, doubled apostrophe escaping, hex/binary literals | -| Identifiers | bracketed identifiers, double-quoted identifiers, backtick identifiers, qualified object names | -| Variables | `@local`, `@@system`, `:host`, `?`, `$1`-style numbered parameters | -| Literals | decimal numbers, scientific notation, hexadecimal numbers, common language constants | -| DDL | common create/alter/drop statements and schema-object declarations | -| DML | select/insert/update/delete/merge/upsert and common query clauses | -| Watcom control flow | `IF`, `THEN`, `ELSEIF`, `ENDIF`, `LOOP`, `LEAVE`, `SIGNAL`, `RESIGNAL`, handlers and exceptions | -| Routines | procedure, proc, function and trigger declarations | -| Transactions | commit, rollback, savepoints and transaction blocks | -| MSSQL surface | `GO`, `CREATE OR ALTER`, `PROC`, variables and basic TRY/CATCH keywords | - -## Design boundaries - -The grammar does not validate SQL. It highlights useful lexical regions and leaves semantic decisions to formatter, object detection, diagnostics and future analyzers. - -Known boundaries: - -- no complete statement grammar -- no schema awareness -- no validation of nested SQL blocks -- no dialect-exclusive error reporting -- no automatic conversion between Watcom SQL and MSSQL diff --git a/docs/WORKSPACE_SETTINGS.md b/docs/WORKSPACE_SETTINGS.md deleted file mode 100644 index a3c3644..0000000 --- a/docs/WORKSPACE_SETTINGS.md +++ /dev/null @@ -1,90 +0,0 @@ -# Workspace settings - -SQLovely settings can be defined globally, per workspace or per workspace folder. The examples below are intended as starting points. - -## Conservative Watcom setup - -```json -{ - "sqlovely.dialect": "watcom", - "sqlovely.format.enabled": true, - "sqlovely.format.keywordCase": "upper", - "sqlovely.format.indentSize": 2, - "sqlovely.format.insertSpaces": true, - "sqlovely.extras.applyWithFormatting": true, - "sqlovely.extras.applyOnSave": false, - "[sql]": { - "editor.defaultFormatter": "tim-mayer.sqlovely" - } -} -``` - -## Format on save - -This uses VS Code's built-in format-on-save setting and SQLovely as the SQL formatter. Enabled SQLovely Extras are applied with formatting by default. - -```json -{ - "sqlovely.dialect": "watcom", - "sqlovely.format.keywordCase": "upper", - "sqlovely.format.indentSize": 2, - "sqlovely.format.insertSpaces": true, - "sqlovely.extras.applyWithFormatting": true, - "[sql]": { - "editor.defaultFormatter": "tim-mayer.sqlovely", - "editor.formatOnSave": true - } -} -``` - -## MSSQL sandbox - -The MSSQL dialect is intentionally rudimentary. Use it for early testing, not as a complete T-SQL implementation. - -```json -{ - "sqlovely.dialect": "mssql", - "sqlovely.extras.applyWithFormatting": true, - "sqlovely.extras.applyOnSave": false, - "[sql]": { - "editor.defaultFormatter": "tim-mayer.sqlovely" - } -} -``` - -## Formatting style - -SQLovely's formatter defaults to a compact SQL style: - -```json -{ - "sqlovely.format.keywordCase": "upper", - "sqlovely.format.indentSize": 2, - "sqlovely.format.insertSpaces": true, - "sqlovely.format.maxConsecutiveBlankLines": 1, - "sqlovely.format.ensureFinalNewline": true -} -``` - -## Extras with formatting - -Enabled SQLovely Extras run with normal SQLovely formatting by default. This means **Format Document**, **SQLovely: Format Current SQL File** and VS Code format-on-save can also insert or update supported metadata headers. - -```json -{ - "sqlovely.extras.applyWithFormatting": true -} -``` - -Set it to `false` to keep formatting limited to whitespace, indentation and keyword casing. The separate `sqlovely.extras.applyOnSave` setting remains disabled by default. - -## Style diagnostics - -SQLovely can report lines that exceed the configured line-length limit. The default limit is 120 characters. - -```json -{ - "sqlovely.diagnostics.maxLineLength.enabled": true, - "sqlovely.diagnostics.maxLineLength.limit": 120 -} -``` diff --git a/images/logo.png b/images/logo.png index 2497570..5dcc3eb 100644 Binary files a/images/logo.png and b/images/logo.png differ diff --git a/package-lock.json b/package-lock.json index de8af4e..56546f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sqlovely", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sqlovely", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "devDependencies": { "@types/node": "^16.18.0", @@ -787,6 +787,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -803,6 +804,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -957,6 +959,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -996,6 +999,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "optional": true, "dependencies": { "base64-js": "^1.3.1", @@ -1150,6 +1154,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1182,6 +1187,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -1191,6 +1197,7 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -1220,6 +1227,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1308,6 +1316,7 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -1372,6 +1381,7 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -1386,6 +1396,7 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -1578,7 +1589,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.20.1", @@ -1633,6 +1645,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1960,6 +1973,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, + "license": "ISC", "optional": true }, "node_modules/ini": { @@ -2391,6 +2405,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2413,6 +2428,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -2614,6 +2630,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, + "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", @@ -2864,6 +2881,7 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -3289,7 +3307,8 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", @@ -3582,6 +3601,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -3671,6 +3691,7 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -3695,6 +3716,7 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" diff --git a/package.json b/package.json index 8f39226..e92a60b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sqlovely", "displayName": "SQLovely", "description": "SQL language support for .sql files, with Watcom SQL as the default dialect and a conservative formatter.", - "version": "0.1.6", + "version": "0.1.7", "publisher": "tim-mayer", "license": "MIT", "repository": { @@ -23,6 +23,7 @@ "onCommand:sqlovely.showActiveDialect", "onCommand:sqlovely.switchDialect", "onCommand:sqlovely.formatCurrentFile", + "onCommand:sqlovely.formatSqlFilesInDirectory", "onCommand:sqlovely.insertOrUpdateMetadataHeader", "onCommand:sqlovely.applyExtras" ], @@ -102,6 +103,11 @@ "category": "SQLovely", "enablement": "editorLangId == sql" }, + { + "command": "sqlovely.formatSqlFilesInDirectory", + "title": "Format SQL Files in Directory", + "category": "SQLovely" + }, { "command": "sqlovely.insertOrUpdateMetadataHeader", "title": "Insert or Update Metadata Header", diff --git a/src/commands/formatSqlFilesInDirectory.ts b/src/commands/formatSqlFilesInDirectory.ts new file mode 100644 index 0000000..72db275 --- /dev/null +++ b/src/commands/formatSqlFilesInDirectory.ts @@ -0,0 +1,180 @@ +import * as vscode from 'vscode'; + +import { COMMANDS } from '../constants'; +import { getActiveDialect, getFormatConfiguration } from '../config'; +import { formatSqlDocument } from '../formatter'; + +interface DirectoryFormatStats { + readonly total: number; + formatted: number; + unchanged: number; + skipped: number; + failed: number; +} + +const SQL_FILE_PATTERN = '**/*.sql'; +const SQL_FILE_EXCLUDE_PATTERN = '{**/.git/**,**/.svn/**,**/.hg/**,**/node_modules/**,**/out/**,**/dist/**}'; + +export function registerFormatSqlFilesInDirectoryCommand(): vscode.Disposable { + return vscode.commands.registerCommand(COMMANDS.formatSqlFilesInDirectory, async () => { + const selectedDirectory = await askForDirectory(); + + if (!selectedDirectory) { + return; + } + + const sqlFiles = await vscode.workspace.findFiles( + new vscode.RelativePattern(selectedDirectory, SQL_FILE_PATTERN), + SQL_FILE_EXCLUDE_PATTERN + ); + + if (sqlFiles.length === 0) { + await vscode.window.showInformationMessage('SQLovely: No .sql files found in the selected directory.'); + return; + } + + const shouldFormat = await confirmDirectoryFormatting(selectedDirectory, sqlFiles.length); + + if (!shouldFormat) { + return; + } + + const stats = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'SQLovely: Formatting SQL files', + cancellable: true + }, + async (progress, token) => formatSqlFiles(sqlFiles, progress, token) + ); + + await vscode.window.showInformationMessage( + `SQLovely: Formatted ${stats.formatted} SQL file(s). ` + + `${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.failed} failed.` + ); + }); +} + +async function askForDirectory(): Promise { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; + const selected = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: workspaceFolder, + openLabel: 'Format SQL files', + title: 'Select a directory containing SQL files' + }); + + return selected?.[0]; +} + +async function confirmDirectoryFormatting(directory: vscode.Uri, fileCount: number): Promise { + const directoryLabel = vscode.workspace.asRelativePath(directory, false); + const action = 'Format SQL files'; + const selection = await vscode.window.showWarningMessage( + `Format ${fileCount} .sql file(s) in "${directoryLabel}"? SQLovely Extras will not be applied by this command.`, + { modal: true }, + action + ); + + return selection === action; +} + +async function formatSqlFiles( + sqlFiles: readonly vscode.Uri[], + progress: vscode.Progress<{ message?: string; increment?: number }>, + token: vscode.CancellationToken +): Promise { + const stats: DirectoryFormatStats = { + total: sqlFiles.length, + formatted: 0, + unchanged: 0, + skipped: 0, + failed: 0 + }; + + const increment = sqlFiles.length > 0 ? 100 / sqlFiles.length : 100; + + for (const uri of sqlFiles) { + if (token.isCancellationRequested) { + stats.skipped += sqlFiles.length - stats.formatted - stats.unchanged - stats.skipped - stats.failed; + break; + } + + progress.report({ + message: vscode.workspace.asRelativePath(uri, false), + increment + }); + + try { + const result = await formatSingleSqlFile(uri); + + if (result === 'formatted') { + stats.formatted += 1; + } else if (result === 'unchanged') { + stats.unchanged += 1; + } else { + stats.skipped += 1; + } + } catch { + stats.failed += 1; + } + } + + return stats; +} + +type SingleFileFormatResult = 'formatted' | 'unchanged' | 'skipped'; + +async function formatSingleSqlFile(uri: vscode.Uri): Promise { + const openDocument = vscode.workspace.textDocuments.find((document) => document.uri.toString() === uri.toString()); + + if (openDocument?.isDirty) { + return 'skipped'; + } + + const formatConfiguration = getFormatConfiguration(uri); + + if (!formatConfiguration.enabled) { + return 'skipped'; + } + + const document = openDocument ?? await vscode.workspace.openTextDocument(uri); + const originalText = document.getText(); + const result = formatSqlDocument(originalText, getActiveDialect(uri), { + keywordCase: formatConfiguration.keywordCase, + indentSize: formatConfiguration.indentSize, + insertSpaces: formatConfiguration.insertSpaces, + maxConsecutiveBlankLines: formatConfiguration.maxConsecutiveBlankLines, + ensureFinalNewline: formatConfiguration.ensureFinalNewline, + applyExtrasWithFormatting: false, + metadataHeaderEnabled: false + }); + + if (!result.changed) { + return 'unchanged'; + } + + const edit = new vscode.WorkspaceEdit(); + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(originalText.length) + ); + + edit.replace(uri, fullRange, result.text); + + const applied = await vscode.workspace.applyEdit(edit); + + if (!applied) { + throw new Error(`Could not apply formatting edit for ${uri.toString()}.`); + } + + const saved = await document.save(); + + if (!saved) { + throw new Error(`Could not save formatted SQL file ${uri.toString()}.`); + } + + return 'formatted'; +} diff --git a/src/commands/index.ts b/src/commands/index.ts index d154f2c..de52a4a 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { registerApplySqlovelyExtrasCommand } from './applyExtras'; import { registerFormatCurrentFileCommand } from './formatCurrentFile'; +import { registerFormatSqlFilesInDirectoryCommand } from './formatSqlFilesInDirectory'; import { registerInsertOrUpdateMetadataHeaderCommand } from './insertOrUpdateMetadataHeader'; import { registerShowActiveDialectCommand } from './showActiveDialect'; import { registerSwitchDialectCommand } from './switchDialect'; @@ -11,6 +12,7 @@ export function registerCommands(context: vscode.ExtensionContext): void { registerShowActiveDialectCommand(), registerSwitchDialectCommand(), registerFormatCurrentFileCommand(), + registerFormatSqlFilesInDirectoryCommand(), registerInsertOrUpdateMetadataHeaderCommand(), registerApplySqlovelyExtrasCommand() ); diff --git a/src/constants.ts b/src/constants.ts index eabfa7a..42cd935 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,7 @@ export const COMMANDS = { showActiveDialect: 'sqlovely.showActiveDialect', switchDialect: 'sqlovely.switchDialect', formatCurrentFile: 'sqlovely.formatCurrentFile', + formatSqlFilesInDirectory: 'sqlovely.formatSqlFilesInDirectory', insertOrUpdateMetadataHeader: 'sqlovely.insertOrUpdateMetadataHeader', applyExtras: 'sqlovely.applyExtras' } as const; diff --git a/test/runProjectValidationTests.js b/test/runProjectValidationTests.js index 450facb..d2e6b3f 100644 --- a/test/runProjectValidationTests.js +++ b/test/runProjectValidationTests.js @@ -96,6 +96,7 @@ runTest('all contributed commands have activation events and compiled handlers', ['sqlovely.showActiveDialect', 'dist/commands/showActiveDialect.js'], ['sqlovely.switchDialect', 'dist/commands/switchDialect.js'], ['sqlovely.formatCurrentFile', 'dist/commands/formatCurrentFile.js'], + ['sqlovely.formatSqlFilesInDirectory', 'dist/commands/formatSqlFilesInDirectory.js'], ['sqlovely.insertOrUpdateMetadataHeader', 'dist/commands/insertOrUpdateMetadataHeader.js'], ['sqlovely.applyExtras', 'dist/commands/applyExtras.js'] ]); @@ -149,10 +150,8 @@ runTest('VSIX packaging scripts are available and keep package output isolated', runTest('documentation and onboarding examples are present', () => { const requiredDocs = [ - 'docs/GETTING_STARTED.md', - 'docs/WORKSPACE_SETTINGS.md', - 'docs/SYNTAX_GRAMMAR.md', - 'docs/SQL_COVERAGE.md', + 'docs/DEVELOPMENT.md', + 'docs/SQL_IMPLEMENTATION.md', 'PACKAGING.md' ]; @@ -165,6 +164,21 @@ runTest('documentation and onboarding examples are present', () => { assert.ok(readme.includes(doc), `README should link to ${doc}`); } assert.ok(readme.includes('MIT')); + + const oldDocs = [ + 'docs/GETTING_STARTED.md', + 'docs/WORKSPACE_SETTINGS.md', + 'docs/SYNTAX_GRAMMAR.md', + 'docs/SQL_COVERAGE.md' + ]; + + for (const doc of oldDocs) { + assert.ok(!exists(doc), `${doc} should be consolidated`); + } + + const contributing = fs.readFileSync(path.join(root, 'CONTRIBUTING.md'), 'utf8'); + assert.ok(contributing.includes('docs/DEVELOPMENT.md')); + assert.ok(contributing.includes('docs/SQL_IMPLEMENTATION.md')); }); runTest('example settings and SQL smoke-test files are valid project assets', () => {