From cf3196f2f37b333cf63db24462ac9aa2d2ba01db Mon Sep 17 00:00:00 2001 From: RedCMD Date: Sat, 28 Sep 2024 16:18:49 +1200 Subject: [PATCH] Improve Formatting --- package.json | 32 +-- src/DiagnosticCollection.ts | 8 +- .../DocumentFormattingEditProvider.ts | 250 +++++++----------- src/TreeSitter.ts | 12 +- src/extension.ts | 4 + src/tree-sitter/tree-sitter-json/grammar.js | 9 +- 6 files changed, 131 insertions(+), 184 deletions(-) diff --git a/package.json b/package.json index e496087..8e89a2e 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,22 @@ "editor.defaultFormatter": "RedCMD.tmlanguage-syntax-highlighter" } }, + "configuration": [ + { + "title": "JSON TextMate", + "properties": { + "tmlanguage-syntax-highlighter.formattingStyle": { + "type": "string", + "enum": [ + "tight", + "default" + ], + "default": "default", + "scope": "resource" + } + } + } + ], "jsonLanguageParticipants": [ { "languageId": "json-textmate" @@ -346,22 +362,6 @@ "category": "TextMate", "icon": "$(inspect)" } - ], - "configuration": [ - { - "title": "TextMate Syntax Highlighting and Intellisense", - "properties": { - "tmlanguage-syntax-highlighter.formattingStyle": { - "type": "string", - "enum": [ - "tight", - "default" - ], - "default": "default", - "scope": "resource" - } - } - } ] }, "dependencies": { diff --git a/src/DiagnosticCollection.ts b/src/DiagnosticCollection.ts index 543d4ec..5a80dd5 100644 --- a/src/DiagnosticCollection.ts +++ b/src/DiagnosticCollection.ts @@ -155,13 +155,7 @@ function Diagnostics(document: vscode.TextDocument) { continue; } diagnostic = { - range: - type == ',' ? - new vscode.Range( - toPosition(node.previousSibling.endPosition), - toPosition(node.previousSibling.endPosition), - ) - : range, + range: toRange(node.previousSibling.endPosition, node.previousSibling.endPosition), message: `'${parentType}' is missing character${type.length > 1 ? 's' : ''} '${type}'`, severity: vscode.DiagnosticSeverity.Error, source: 'TreeSitter', diff --git a/src/Providers/DocumentFormattingEditProvider.ts b/src/Providers/DocumentFormattingEditProvider.ts index 2029d14..26b28b8 100644 --- a/src/Providers/DocumentFormattingEditProvider.ts +++ b/src/Providers/DocumentFormattingEditProvider.ts @@ -1,16 +1,25 @@ import * as vscode from 'vscode'; import * as Parser from 'web-tree-sitter'; import { getTrees, toRange, toPoint, queryNode } from "../TreeSitter"; +import { sleep } from "../extension"; -/* - const filesConfig = workspace.getConfiguration('files', document); - const fileFormattingOptions = { - trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), - trimFinalNewlines: filesConfig.get('trimFinalNewlines'), - insertFinalNewline: filesConfig.get('insertFinalNewline'), +type formattingStyle = { + tabType: ' ' | '\t'; + tabSize: number; + wsBrackets: string; +}; + +function getFormattingStyle(options: vscode.FormattingOptions): formattingStyle { + const styleName: 'tight' | 'default' = vscode.workspace.getConfiguration('tmlanguage-syntax-highlighter').get('formattingStyle'); + const style: formattingStyle = { + tabType: options.insertSpaces ? ' ' : '\t', + tabSize: options.insertSpaces ? options.tabSize : 1, + wsBrackets: styleName == 'tight' ? '' : ' ', }; - */ + return style; +} + export const DocumentFormattingEditProvider: vscode.DocumentFormattingEditProvider = { provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.TextEdit[] { @@ -19,15 +28,12 @@ export const DocumentFormattingEditProvider: vscode.DocumentFormattingEditProvid const jsonTree = trees.jsonTree; const textEdits: vscode.TextEdit[] = []; - const tabType = options.insertSpaces ? ' ' : '\t'; - const tabSize = options.insertSpaces ? options.tabSize : 1; - const style = getFormattingStyle(); + const style = getFormattingStyle(options); // const start = performance.now(); - parseAllChildren(jsonTree.rootNode, textEdits, 0, tabSize, tabType, style); - // vscode.window.showInformationMessage(performance.now() - start + "ms"); + formatChildren(jsonTree.rootNode, textEdits, 0, style); - // vscode.window.showInformationMessage(JSON.stringify(textEdits)); + // vscode.window.showInformationMessage(`Format ${(performance.now() - start).toFixed(3)}ms\n${JSON.stringify(textEdits)}`); return textEdits; }, }; @@ -35,13 +41,12 @@ export const DocumentFormattingEditProvider: vscode.DocumentFormattingEditProvid export const DocumentRangeFormattingEditProvider: vscode.DocumentRangeFormattingEditProvider = { provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.TextEdit[] { // vscode.window.showInformationMessage(JSON.stringify("FormatRange")); + // const start = performance.now(); const trees = getTrees(document); const jsonTree = trees.jsonTree; const textEdits: vscode.TextEdit[] = []; - const tabType = options.insertSpaces ? ' ' : '\t'; - const tabSize = options.insertSpaces ? options.tabSize : 1; - const style = getFormattingStyle(); + const style = getFormattingStyle(options); const startPoint = toPoint(range.start); const endPoint = toPoint(range.end); @@ -57,27 +62,28 @@ export const DocumentRangeFormattingEditProvider: vscode.DocumentRangeFormatting break; } node = nestedNode; - level += tabSize; + level += style.tabSize; } const indent = Math.min(level, node.startPosition.column); - parseAllChildren(node, textEdits, indent, tabSize, tabType, style); + formatChildren(node, textEdits, indent, style); - // vscode.window.showInformationMessage(JSON.stringify(textEdits)); + // vscode.window.showInformationMessage(`FormatRange ${(performance.now() - start).toFixed(3)}ms\n${JSON.stringify(textEdits)}`); return textEdits; }, }; export const OnTypeFormattingEditProvider: vscode.OnTypeFormattingEditProvider = { - provideOnTypeFormattingEdits(document: vscode.TextDocument, position: vscode.Position, ch: string, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.TextEdit[] { - // vscode.window.showInformationMessage(JSON.stringify("FormatRange")); + async provideOnTypeFormattingEdits(document: vscode.TextDocument, position: vscode.Position, ch: string, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise { + // vscode.window.showInformationMessage(JSON.stringify("FormatType")); + // const start = performance.now(); + await sleep(50); // partially avoids race condition with reparseTextDocument() + const trees = getTrees(document); const jsonTree = trees.jsonTree; const textEdits: vscode.TextEdit[] = []; - const tabType = options.insertSpaces ? ' ' : '\t'; - const tabSize = options.insertSpaces ? options.tabSize : 1; - const style = getFormattingStyle(); + const style = getFormattingStyle(options); const startPoint = toPoint(position.translate(0, -1)); const endPoint = toPoint(position); @@ -97,6 +103,7 @@ export const OnTypeFormattingEditProvider: vscode.OnTypeFormattingEditProvider = break; case '}': case ']': + case ':': node = cursorNode.parent; break; default: @@ -112,31 +119,19 @@ export const OnTypeFormattingEditProvider: vscode.OnTypeFormattingEditProvider = const indent = Math.min(level, node.startPosition.column); - parseAllChildren(node, textEdits, indent, tabSize, tabType, style); + formatChildren(node, textEdits, indent, style); - // vscode.window.showInformationMessage(JSON.stringify(textEdits)); + // vscode.window.showInformationMessage(`FormatType ${(performance.now() - start).toFixed(3)}ms\n${JSON.stringify(textEdits)}`); return textEdits; }, }; -type formattingStyle = { wsBrackets: string; }; -function getFormattingStyle(): formattingStyle { - const styleName: 'tight' | 'default' = vscode.workspace.getConfiguration('tmlanguage-syntax-highlighter').get('formattingStyle'); - const style = { - default: { wsBrackets: ' ' }, - tight: { wsBrackets: '' }, - }[styleName]; - return style; -} -function parseAllChildren(parentNode: Parser.SyntaxNode, textEdits: vscode.TextEdit[], indent: number, tabSize: number, tabType: string, style: formattingStyle) { - let range: vscode.Range; - let whiteSpace: string; - let textEdit: vscode.TextEdit; - let expand: Boolean = false; +function formatChildren(parentNode: Parser.SyntaxNode, textEdits: vscode.TextEdit[], indent: number, style: formattingStyle): boolean { + let expand: boolean = false; for (const node of parentNode.namedChildren) { - if (parseAllChildren(node, textEdits, indent + tabSize, tabSize, tabType, style)) { + if (formatChildren(node, textEdits, indent + style.tabSize, style)) { expand = true; } } @@ -160,7 +155,7 @@ function parseAllChildren(parentNode: Parser.SyntaxNode, textEdits: vscode.TextE expand = true; break; } - + /* FallThrough */ default: if (namedChildCount > 2) { expand = true; @@ -180,140 +175,89 @@ function parseAllChildren(parentNode: Parser.SyntaxNode, textEdits: vscode.TextE for (const node of parentNode.children) { - if (node.isMissing) { - range = toRange(node); - - textEdit = vscode.TextEdit.replace(range, node.type); - textEdits.push(textEdit); + if (node.isError) { + continue; } + + const nextSibling = node.nextSibling; + const prevSibling = node.previousSibling; + switch (node.type) { case '{': case '[': - indent += tabSize; - - if (expand == true) - whiteSpace = '\n'.padEnd(indent + 1, tabType); - else - whiteSpace = style.wsBrackets; + indent += style.tabSize; - if (node.isMissing) { - range = toRange(node); - } - else { - range = new vscode.Range( - node.endPosition.row, - node.endPosition.column, - node.nextSibling?.startPosition.row ?? parentNode.endPosition.row, - node.nextSibling?.startPosition.column ?? parentNode.endPosition.column, - ); + if (nextSibling?.type == '}' || nextSibling?.type == ']') { // Don't double up spaces in empty objects/arrays + break; } - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); + textEdits.push( // Whitespace after opening bracket + vscode.TextEdit.replace( + toRange( + node.startPosition, + nextSibling?.startPosition ?? parentNode.endPosition, + ), + expand ? + node.type + '\n'.padEnd(indent + 1, style.tabType) : + node.type + style.wsBrackets, + ), + ); break; case '}': case ']': - indent -= tabSize; - - if (node.previousSibling?.type == '{') { - break; - } - if (node.previousSibling?.type == '[') { - break; - } - - if (expand == true) - whiteSpace = '\n'.padEnd(indent + 1, tabType); - else - whiteSpace = style.wsBrackets; - - range = new vscode.Range( - node.previousSibling?.endPosition.row ?? parentNode.startPosition.row, - node.previousSibling?.endPosition.column ?? parentNode.startPosition.column, - node.startPosition.row, - node.startPosition.column, + indent -= style.tabSize; + + textEdits.push( // Whitespace before closing bracket + vscode.TextEdit.replace( + toRange( + prevSibling?.endPosition ?? parentNode.startPosition, + node.endPosition, + ), + expand ? + '\n'.padEnd(indent + 1, style.tabType) + node.type : + style.wsBrackets + node.type, + ), ); - - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); break; case ',': - if (node.nextSibling?.type != '}' && node.nextSibling?.type != ']' || node.isMissing) { // hacks upon hacks - if (expand == true) - whiteSpace = '\n'.padEnd(indent + 1, tabType); - else - whiteSpace = ' '; - - if (node.isMissing) { - range = toRange(node); // node.nextSibling doesn't work very well on missing nodes - } - else { - range = new vscode.Range( - node.endPosition.row, - node.endPosition.column, - node.nextSibling?.startPosition.row ?? parentNode.endPosition.row, - node.nextSibling?.startPosition.column ?? parentNode.endPosition.column, - ); - } - - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); - } - - if (node.previousSibling?.type == '{') { - break; - } - if (node.previousSibling?.type == '[') { - break; - } - if (node.previousSibling?.type == ',') { + if (prevSibling?.type == '{' || prevSibling?.type == '[' || nextSibling?.type == '}' || nextSibling?.type == ']' || nextSibling?.type == ',') { + textEdits.push( // Remove extra comma + vscode.TextEdit.delete( + toRange(node), + ), + ); break; } - whiteSpace = ''; - - range = new vscode.Range( - node.previousSibling?.endPosition.row ?? parentNode.startPosition.row, - node.previousSibling?.endPosition.column ?? parentNode.startPosition.column, - node.startPosition.row, - node.startPosition.column, + textEdits.push( // No whitespace before comma. Only whitespace after comma + vscode.TextEdit.replace( + toRange( + prevSibling?.endPosition ?? parentNode.startPosition, + node.isMissing ? // node.nextSibling doesn't work very well on missing nodes + node.endPosition : + nextSibling?.startPosition ?? parentNode.endPosition, + ), + expand ? + ',\n'.padEnd(indent + 2, style.tabType) : + ', ', + ), ); - - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); break; case ':': - whiteSpace = ' '; - - if (node.isMissing) { - range = toRange(node); - } - else { - range = new vscode.Range( - node.endPosition.row, - node.endPosition.column, - node.nextSibling?.startPosition.row ?? parentNode.endPosition.row, - node.nextSibling?.startPosition.column ?? parentNode.endPosition.column, - ); - } - - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); - - whiteSpace = ''; - - range = new vscode.Range( - node.previousSibling?.endPosition.row ?? parentNode.startPosition.row, - node.previousSibling?.endPosition.column ?? parentNode.startPosition.column, - node.startPosition.row, - node.startPosition.column, + textEdits.push( // One space after colon + vscode.TextEdit.replace( + toRange( + prevSibling?.endPosition ?? parentNode.startPosition, + node.isMissing ? // node.nextSibling doesn't work very well on missing nodes + node.endPosition : + nextSibling?.startPosition ?? parentNode.endPosition, + ), + ': ', + ) ); - - textEdit = vscode.TextEdit.replace(range, whiteSpace); - textEdits.push(textEdit); break; } } diff --git a/src/TreeSitter.ts b/src/TreeSitter.ts index c45cd72..d097751 100644 --- a/src/TreeSitter.ts +++ b/src/TreeSitter.ts @@ -146,12 +146,14 @@ export function queryNode(node: Parser.SyntaxNode, queryString: string, startPoi return queryCaptures; } -export function toRange(node: Parser.SyntaxNode): vscode.Range { +export function toRange(node: Parser.SyntaxNode): vscode.Range; +export function toRange(start: Parser.Point, end: Parser.Point): vscode.Range; +export function toRange(node: Parser.SyntaxNode | Parser.Point, end?: Parser.Point): vscode.Range { if (!node) { return null; } - const startPosition = node.startPosition; - const endPosition = node.endPosition; + const startPosition = 'startPosition' in node ? node.startPosition : node; + const endPosition = 'startPosition' in node ? node.endPosition : end; const range = new vscode.Range( startPosition.row, startPosition.column, @@ -453,7 +455,7 @@ function reparseTextDocument(edits: vscode.TextDocumentChangeEvent) { const oldRegexTreesIterator = oldRegexTrees.values(); let skip = false; let oldRegexTree: Parser.Tree; - let oldRegexTreeCopy: Parser.Tree; + // let oldRegexTreeCopy: Parser.Tree; const queryCaptures = queryNode(jsonTree.rootNode, `(regex) @regex`); // vscode.window.showInformationMessage(`Old: ${(performance.now() - start).toFixed(3)}ms ${JSON.stringify(oldRegexTrees.size)}`); @@ -483,7 +485,7 @@ function reparseTextDocument(edits: vscode.TextDocumentChangeEvent) { break; } - oldRegexTreeCopy = oldRegexTree.copy(); + // oldRegexTreeCopy = oldRegexTree.copy(); for (const delta of deltas) { oldRegexTree.edit(delta); } diff --git a/src/extension.ts b/src/extension.ts index 0af44ab..1c17c69 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -71,6 +71,10 @@ export function deactivate() { } +export function sleep(miliseconds: number) { + return new Promise(resolve => setTimeout(resolve, miliseconds)); +} + export function stringify(this: any, key: string, value: any) { if (typeof value === 'function') { return ""; diff --git a/src/tree-sitter/tree-sitter-json/grammar.js b/src/tree-sitter/tree-sitter-json/grammar.js index 0e713b0..f77121f 100644 --- a/src/tree-sitter/tree-sitter-json/grammar.js +++ b/src/tree-sitter/tree-sitter-json/grammar.js @@ -504,12 +504,16 @@ module.exports = grammar({ ), ), - _comma: $ => repeat1( + _comma: $ => repeat1( // Is able to insert `missing` comma seq( repeat($._whitespace), ',', ), ), + _colon: $ => seq( // Is able to insert `missing` colon + repeat($._whitespace), + ':', + ), }, }); @@ -599,10 +603,9 @@ function pair($, key, value) { ), ), ), - repeat($._whitespace), optional( seq( - ':', + $._colon, repeat($._whitespace), optional( // TS bad at error recovery choice(