Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/formatter #279

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f491c7b
Initial commit
worksofliam Mar 18, 2024
6589f39
Use statement groups in the formatter
worksofliam May 13, 2024
b1cb1b0
Initial formatting work
Jul 4, 2024
2df1989
Merge branch 'main' into feature/formatter
worksofliam Aug 22, 2024
761864d
Merge branch 'main' into feature/formatter
worksofliam Aug 26, 2024
b65555c
Recursive formatter
worksofliam Aug 29, 2024
6008ccd
Add case options back
worksofliam Aug 29, 2024
f3893a9
Improvements for subblocks
worksofliam Aug 29, 2024
0a4052c
Fix to tokeniser to better support ORDER BY
worksofliam Aug 29, 2024
3f30c23
Remove unneeded spaces from test results
worksofliam Aug 29, 2024
f45e339
Test the formatter on existing tests
worksofliam Aug 29, 2024
c241335
Apply formatter tests
worksofliam Aug 30, 2024
b5be163
Merge branch 'main' into feature/formatter
worksofliam Sep 18, 2024
b0841c6
Fix tests for formatter support
worksofliam Sep 19, 2024
c78cad4
Major cleanup of formatter
worksofliam Sep 19, 2024
7645c72
Fix issue with spaces
worksofliam Sep 19, 2024
03a9b76
Refactor isBlock to isCompound
worksofliam Sep 20, 2024
dc371e8
Start of support for statement label
worksofliam Sep 20, 2024
60591fd
Support for more statement types
worksofliam Sep 20, 2024
7742046
Start of condition support
worksofliam Sep 20, 2024
4b6b4e6
Fix bug with parameters
worksofliam Sep 20, 2024
fd12b6a
Refactor formatProvider and formatter to use indentWidth instead of t…
worksofliam Sep 20, 2024
801a6a1
Formatter support for conditional statements
worksofliam Sep 20, 2024
4b89edc
Support for improved spacing
worksofliam Sep 20, 2024
296a251
Formatter settings
worksofliam Sep 21, 2024
258e6eb
Support for references to objects in column list
worksofliam Sep 22, 2024
d6e287e
Remove logs
worksofliam Sep 22, 2024
008e435
Improvements to show a signature
worksofliam Sep 23, 2024
6c3ee29
Fix test cases
worksofliam Sep 23, 2024
04b43eb
Added test cases
Sep 23, 2024
9ae112b
Additional test for case support
worksofliam Oct 3, 2024
cda50e7
Merge branch 'main' into feature/formatter
worksofliam Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,30 +100,30 @@
"properties": {
"vscode-db2i.sqlFormat.identifierCase": {
"type": "string",
"description": "SQL identifiers",
"description": "Format SQL identifiers into a certain case",
"default": "preserve",
"enum": [
"lower",
"upper",
"preserve"
],
"enumDescriptions": [
"Format SQL identifiers in lowercase",
"Format SQL identifiers in uppercase",
"Preserve the current formatting of SQL identifiers"
]
},
"vscode-db2i.sqlFormat.keywordCase": {
"type": "string",
"description": "SQL keywords",
"description": "Format SQL keywords into a certain case",
"default": "lower",
"enum": [
"lower",
"upper"
],
"enumDescriptions": [
"Format reserved SQL keywords in lowercase",
"Format reserved SQL keywords in uppercase"
]
},
"vscode-db2i.sqlFormat.spaceBetweenStatements": {
"type": "string",
"description": "Add space between statements.",
"default": "true",
"enum": [
"true",
"false"
]
}
}
Expand Down Expand Up @@ -1247,4 +1247,4 @@
}
]
}
}
}
22 changes: 11 additions & 11 deletions src/contributes.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,30 @@
"properties": {
"vscode-db2i.sqlFormat.identifierCase": {
"type": "string",
"description": "SQL identifiers",
"description": "Format SQL identifiers into a certain case",
"default": "preserve",
"enum": [
"lower",
"upper",
"preserve"
],
"enumDescriptions": [
"Format SQL identifiers in lowercase",
"Format SQL identifiers in uppercase",
"Preserve the current formatting of SQL identifiers"
]
},
"vscode-db2i.sqlFormat.keywordCase": {
"type": "string",
"description": "SQL keywords",
"description": "Format SQL keywords into a certain case",
"default": "lower",
"enum": [
"lower",
"upper"
],
"enumDescriptions": [
"Format reserved SQL keywords in lowercase",
"Format reserved SQL keywords in uppercase"
]
},
"vscode-db2i.sqlFormat.spaceBetweenStatements": {
"type": "string",
"description": "Add space between statements.",
"default": "true",
"enum": [
"true",
"false"
]
}
}
Expand Down
13 changes: 10 additions & 3 deletions src/language/providers/formatProvider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Position, Range, TextEdit, languages } from "vscode";
import Statement from "../../database/statement";
import { formatSql, CaseOptions } from "../sql/formatter";
import Configuration from "../../configuration";

export const formatProvider = languages.registerDocumentFormattingEditProvider({language: `sql`}, {
async provideDocumentFormattingEdits(document, options, token) {
const formatted = Statement.format(
const identifierCase: CaseOptions = <CaseOptions>(Configuration.get(`sqlFormat.identifierCase`) || `preserve`);
const keywordCase: CaseOptions = <CaseOptions>(Configuration.get(`sqlFormat.keywordCase`) || `lower`);
const spaceBetweenStatements: string = (Configuration.get(`sqlFormat.spaceBetweenStatements`) || `false`);
const formatted = formatSql(
document.getText(),
{
useTabs: !options.insertSpaces,
tabWidth: options.tabSize,
indentWidth: options.tabSize,
identifierCase,
keywordCase,
spaceBetweenStatements: spaceBetweenStatements === `true`
}
);

Expand Down
23 changes: 15 additions & 8 deletions src/language/providers/hoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,21 @@ export const hoverProvider = languages.registerHoverProvider({ language: `sql` }
if (result) {
if ('routine' in result) {
const routineOffset = ref.tokens[ref.tokens.length-1].range.end+1;
const callableRef = statementAt.getCallableDetail(routineOffset, false);
if (callableRef) {
const { currentCount } = getPositionData(callableRef, routineOffset);
const signatures = await DbCache.getCachedSignatures(callableRef.parentRef.object.schema, callableRef.parentRef.object.name);
const possibleSignatures = signatures.filter((s) => s.parms.length >= currentCount).sort((a, b) => a.parms.length - b.parms.length);
const signature = possibleSignatures.find((signature) => currentCount <= signature.parms.length);
if (signature) {
addRoutineMd(md, signature, result);
const callableRef = statementAt.getCallableDetail(routineOffset, false)
const signatures = await DbCache.getCachedSignatures(schema, ref.object.name);

if (signatures.length > 0) {
let chosenSignature: CallableSignature | undefined;
if (callableRef) {
const { currentCount } = getPositionData(callableRef, routineOffset);
const possibleSignatures = signatures.filter((s) => s.parms.length >= currentCount).sort((a, b) => a.parms.length - b.parms.length);
chosenSignature = possibleSignatures.find((signature) => currentCount <= signature.parms.length);
} else {
chosenSignature = signatures[0];
}

if (chosenSignature) {
addRoutineMd(md, chosenSignature, result);
}
}
} else {
Expand Down
194 changes: 194 additions & 0 deletions src/language/sql/formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import Document from "./document";
import { StatementGroup, StatementType, StatementTypeWord, Token } from "./types";
import SQLTokeniser from "./tokens";

export declare type CaseOptions = `preserve` | `upper` | `lower`;

export interface FormatOptions {
indentWidth?: number; // Defaults to 4
keywordCase?: CaseOptions;
identifierCase?: CaseOptions;
newLineLists?: boolean;
spaceBetweenStatements?: boolean
}

const SINGLE_LINE_STATEMENT_TYPES: StatementType[] = [StatementType.Create, StatementType.Declare, StatementType.Set, StatementType.Delete, StatementType.Call, StatementType.If, StatementType.End];

export function formatSql(textDocument: string, options: FormatOptions = {}): string {
let result: string[] = [];
let document = new Document(textDocument);
const statementGroups: StatementGroup[] = document.getStatementGroups();

const eol = textDocument.includes(`\r\n`) ? `\r\n` : `\n`;
let prevType = StatementType.Unknown;

for (const statementGroup of statementGroups) {
let currentIndent = 0;
for (let i = 0; i < statementGroup.statements.length; i++) {
const statement = statementGroup.statements[i];
const withBlocks = SQLTokeniser.createBlocks(statement.tokens);

if (statement.isCompoundEnd() || statement.isConditionEnd()) {
currentIndent -= 4;
}

result.push(...formatTokens(withBlocks, options).map(l => ``.padEnd(currentIndent) + l));
if (!statement.isCompoundStart()) {
result[result.length-1] += `;`
}

if (options.spaceBetweenStatements) {
if (prevType !== statement.type && i < statementGroup.statements.length - 1) {
result.push(``);
}
}

if (statement.isCompoundStart() || statement.isConditionStart()) {
currentIndent += 4;
}

prevType = statement.type;
}
}

return result
.map((line) => (line[0] === eol ? line.substring(1) : line))
.join(eol)
}

function formatTokens(tokensWithBlocks: Token[], options: FormatOptions): string[] {
let possibleType = StatementType.Unknown;
const indent = options.indentWidth || 4;
let currentIndent = 0;
let newLines: string[] = [``];
let typeToken: Token;

if (tokensWithBlocks.length > 2 && tokensWithBlocks[1].type === `colon`) {
typeToken = tokensWithBlocks[2];
} else {
typeToken = tokensWithBlocks[0];
}

if (typeToken && typeToken.value) {
possibleType = StatementTypeWord[typeToken.value.toUpperCase()] || StatementType.Unknown;
}


const isSingleLineOnly = SINGLE_LINE_STATEMENT_TYPES.includes(possibleType);

const getSpacing = () => {
return ``.padEnd(currentIndent);
}

const lastLine = () => {
return newLines[newLines.length-1];
}

const append = (newContent: string) => {
newLines[newLines.length-1] = newLines[newLines.length-1] + newContent;
}

const newLine = (indentLevelChange = 0) => {
currentIndent += (indentLevelChange * indent);
newLines.push(getSpacing());
}

const addSublines = (lines: string[]) => {
newLines.push(...lines.map(l => ``.padEnd(currentIndent + indent) + l));
newLine();
}

for (let i = 0; i < tokensWithBlocks.length; i++) {
const cT = tokensWithBlocks[i];
const nT = tokensWithBlocks[i + 1];
const pT = tokensWithBlocks[i - 1];

const currentLine = lastLine();
const needsSpace = (currentLine.trim().length !== 0 && !currentLine.endsWith(` `)) && pT?.type !== `dot` && i > 0;

switch (cT.type) {
case `block`:
if (cT.block) {
const hasClauseOrStatement = tokenIs(cT.block[0], `statementType`);
const commaCount = cT.block.filter(t => tokenIs(t, `comma`)).length;
const containsSubBlock = cT.block.some(t => t.type === `block`);

if (cT.block.length === 1) {
append(`(${cT.block![0].value})`);

} else if (hasClauseOrStatement || containsSubBlock) {
append(` (`);
addSublines(formatTokens(cT.block!, options));
append(`)`);
} else if (commaCount >= 2) {
append(`(`)
addSublines(formatTokens(cT.block!, {...options, newLineLists: true}));
append(`)`);
} else {
const formattedSublines = formatTokens(cT.block!, options);
if (formattedSublines.length === 1 && possibleType !== StatementType.Create) {
append(`(${formattedSublines[0]})`);
} else {
append(`(`)
addSublines(formattedSublines);
append(`)`);
}
}
} else {
throw new Error(`Block token without block`);
}
break;
case `dot`:
append(cT.value);
break;
case `comma`:
append(cT.value);

if (options.newLineLists) {
newLine();
}
break;

case `sqlName`:
if (needsSpace) {
append(` `);
}

append(cT.value);
break;

default:
const isKeyword = ((tokenIs(cT, `statementType`) || tokenIs(cT, `clause`)));
if (isKeyword && i > 0 && isSingleLineOnly === false) {
newLine(options.newLineLists ? -1 : 0);
}

else if (needsSpace) {
append(` `);
}

append(transformCase(cT, cT.type === `word` ? options.identifierCase : options.keywordCase));

if (options.newLineLists && isKeyword && isSingleLineOnly === false) {
newLine(1);
}
break;
}
}

return newLines;
}

const tokenIs = (token: Token|undefined, type: string, value?: string) => {
return (token && token.type === type && (value ? token.value?.toUpperCase() === value : true));
}

const transformCase = (token: Token|undefined, stringCase: CaseOptions|undefined) => {
if (stringCase == `upper`) {
return token.value.toUpperCase();
} else if (stringCase == `lower`) {
return token.value.toLowerCase();
} else {
return token.value;
}
}
2 changes: 2 additions & 0 deletions src/language/sql/tests/blocks.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@

import { describe, expect, test } from 'vitest'
import Document from '../document';
import { formatSql } from '../formatter';

const parserScenarios = describe.each([
{newDoc: (content: string) => new Document(content), isFormatted: false},
{newDoc: (content: string) => new Document(formatSql(content, {newLineLists: true})), isFormatted: true}
]);

parserScenarios(`Block statement tests`, ({newDoc, isFormatted}) => {
Expand Down
Loading
Loading