From 1e91b9a5ff492d2511f7b39345441a053c70ee2f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 3 Feb 2025 15:46:13 +0100 Subject: [PATCH] Added command to only format selected comments --- package-lock.json | 4 +- package.json | 2 +- src/commands/commands.ts | 3 +- .../formatComments/BaseCommentFormatter.ts | 26 +++++-------- src/commands/formatComments/XmlFormatter.ts | 26 ++++--------- src/commands/formatComments/formatComments.ts | 19 ++++++++-- .../registerFormatCommentsOnSave.ts | 4 +- src/commands/formatComments/types.ts | 8 ++-- src/commands/formatComments/utils.ts | 38 +++++++++++++++++++ src/commands/registerCommands.ts | 3 +- src/test/formatComments.test.ts | 30 ++++++++++++--- 11 files changed, 107 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index e25c83c..1cee021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "andreas-talon", - "version": "3.65.0", + "version": "3.67.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "andreas-talon", - "version": "3.65.0", + "version": "3.67.0", "license": "MIT", "dependencies": { "ignore": "^5.2.4", diff --git a/package.json b/package.json index 78a87d5..76ddfec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "andreas-talon", "displayName": "Andreas Talon", "description": "VSCode extension used by Talon Voice", - "version": "3.66.0", + "version": "3.67.0", "publisher": "AndreasArvidsson", "license": "MIT", "main": "./out/extension.js", diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 00195e4..6f5821c 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -74,7 +74,8 @@ export const commandDescriptions = { ), increment: visible("Edit", "Increment selected number.", undefined, "(value?: number)"), decrement: visible("Edit", "Decrement selected number.", undefined, "(value?: number)"), - formatComments: makePrivate("Edit", "Format comments in active file."), + formatComments: makePrivate("Edit", "Format selected comments"), + formatAllComments: makePrivate("Edit", "Format comments in active file."), // Navigation commands openEditorAtIndex: visible( diff --git a/src/commands/formatComments/BaseCommentFormatter.ts b/src/commands/formatComments/BaseCommentFormatter.ts index 58f1a73..37f1377 100644 --- a/src/commands/formatComments/BaseCommentFormatter.ts +++ b/src/commands/formatComments/BaseCommentFormatter.ts @@ -1,6 +1,7 @@ -import * as vscode from "vscode"; +import type { Selection, TextDocument } from "vscode"; +import { Range } from "vscode"; import type { Change, CommentFormatter, CommentMatch, Line } from "./types"; -import { isValidLine, parseTokens } from "./utils"; +import { isValidLine, matchAll, parseTokens } from "./utils"; export abstract class BaseCommentFormatter implements CommentFormatter { protected abstract regex: RegExp; @@ -10,8 +11,7 @@ export abstract class BaseCommentFormatter implements CommentFormatter { protected abstract parseMatch(match: RegExpExecArray): CommentMatch; - public parse(document: vscode.TextDocument): Change[] { - const matches = document.getText().matchAll(this.regex); + public parse(document: TextDocument, selections?: readonly Selection[]): Change[] { const changes: Change[] = []; const unprocessedLines: Line[] = []; @@ -26,17 +26,9 @@ export abstract class BaseCommentFormatter implements CommentFormatter { unprocessedLines.length = 0; }; - for (const match of matches) { - if (match.index == null) { - continue; - } + matchAll(document, selections, this.regex, (match, range) => { const matchText = match[0]; - const range = new vscode.Range( - document.positionAt(match.index), - document.positionAt(match.index + matchText.length) - ); - - const { text, isBlockComment } = this.parseMatch(match as RegExpExecArray); + const { text, isBlockComment } = this.parseMatch(match); const indentation = matchText.slice(0, matchText.length - text.length); if (isBlockComment) { @@ -44,7 +36,7 @@ export abstract class BaseCommentFormatter implements CommentFormatter { if (newText != null) { changes.push({ range, text: newText }); } - continue; + return; } // Non consecutive line comments. Process the previous lines. @@ -57,7 +49,7 @@ export abstract class BaseCommentFormatter implements CommentFormatter { } unprocessedLines.push({ range, text, indentation }); - } + }); // Process any remaining lines if (unprocessedLines.length > 0) { @@ -68,7 +60,7 @@ export abstract class BaseCommentFormatter implements CommentFormatter { } protected abstract parseBlockComment( - range: vscode.Range, + range: Range, text: string, indentation: string ): string | undefined; diff --git a/src/commands/formatComments/XmlFormatter.ts b/src/commands/formatComments/XmlFormatter.ts index 5b94003..9246692 100644 --- a/src/commands/formatComments/XmlFormatter.ts +++ b/src/commands/formatComments/XmlFormatter.ts @@ -1,6 +1,7 @@ -import * as vscode from "vscode"; +import type { Selection, TextDocument } from "vscode"; +import { Range } from "vscode"; import type { Change, CommentFormatter, CommentMatch } from "./types"; -import { isValidLine, parseTokens } from "./utils"; +import { isValidLine, matchAll, parseTokens } from "./utils"; const prefix = ""; @@ -10,20 +11,11 @@ export class XmlFormatter implements CommentFormatter { constructor(private lineWidth: number) {} - public parse(document: vscode.TextDocument): Change[] { - const matches = document.getText().matchAll(this.regex); + public parse(document: TextDocument, selections?: readonly Selection[]): Change[] { const changes: Change[] = []; - for (const match of matches) { - if (match.index == null) { - continue; - } + matchAll(document, selections, this.regex, (match, range) => { const matchText = match[0]; - const range = new vscode.Range( - document.positionAt(match.index), - document.positionAt(match.index + matchText.length) - ); - const text = match[1]; const indentation = matchText.slice(0, matchText.length - text.length); @@ -31,7 +23,7 @@ export class XmlFormatter implements CommentFormatter { if (newText != null) { changes.push({ range, text: newText }); } - } + }); return changes; } @@ -42,11 +34,7 @@ export class XmlFormatter implements CommentFormatter { return { text, isBlockComment }; } - private parseBlockComment( - range: vscode.Range, - text: string, - indentation: string - ): string | undefined { + private parseBlockComment(range: Range, text: string, indentation: string): string | undefined { // Extract the text between the "" const textContent = text.slice(prefix.length, -suffix.length); const linePrefix = ""; diff --git a/src/commands/formatComments/formatComments.ts b/src/commands/formatComments/formatComments.ts index 3b517c3..75fee79 100644 --- a/src/commands/formatComments/formatComments.ts +++ b/src/commands/formatComments/formatComments.ts @@ -9,21 +9,32 @@ import { PythonFormatter } from "./PythonFormatter"; import type { CommentFormatter } from "./types"; import { XmlFormatter } from "./XmlFormatter"; +interface Properties { + editor: vscode.TextEditor; + doSave?: boolean; + onlySelected?: boolean; +} + export function formatComments(): Promise { - const editor = getActiveEditor(); - return formatCommentsForEditor(editor); + return formatCommentsRunner({ editor: getActiveEditor(), onlySelected: true }); +} + +export function formatAllComments(): Promise { + return formatCommentsRunner({ editor: getActiveEditor() }); } -export async function formatCommentsForEditor(editor: vscode.TextEditor, doSave = false) { +export async function formatCommentsRunner(properties: Properties): Promise { + const { editor, doSave, onlySelected } = properties; const { document } = editor; const lineWidth = await getLineWidth(document); const configuration = getFormatter(document.languageId, lineWidth); + const selections = onlySelected ? editor.selections : undefined; if (configuration == null) { return; } - const changes = configuration.parse(document); + const changes = configuration.parse(document, selections); if (changes.length === 0) { return; diff --git a/src/commands/formatComments/registerFormatCommentsOnSave.ts b/src/commands/formatComments/registerFormatCommentsOnSave.ts index be37513..0c500fe 100644 --- a/src/commands/formatComments/registerFormatCommentsOnSave.ts +++ b/src/commands/formatComments/registerFormatCommentsOnSave.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { formatCommentsForEditor } from "./formatComments"; import { configuration } from "../../util/configuration"; +import { formatCommentsRunner } from "./formatComments"; export function registerFormatCommentsOnSave(): vscode.Disposable { // onWillSaveTextDocument does not tree ge on "Save without formatting" @@ -11,7 +11,7 @@ export function registerFormatCommentsOnSave(): vscode.Disposable { ) { const editor = vscode.window.visibleTextEditors.find((e) => e.document === e.document); if (editor != null) { - await formatCommentsForEditor(editor, true); + await formatCommentsRunner({ editor, doSave: true }); } } }); diff --git a/src/commands/formatComments/types.ts b/src/commands/formatComments/types.ts index 705a18b..e61ea13 100644 --- a/src/commands/formatComments/types.ts +++ b/src/commands/formatComments/types.ts @@ -1,7 +1,7 @@ -import * as vscode from "vscode"; +import type { Range, Selection, TextDocument } from "vscode"; export interface CommentFormatter { - parse(document: vscode.TextDocument): Change[]; + parse(document: TextDocument, selections?: readonly Selection[]): Change[]; } export interface CommentMatch { @@ -10,14 +10,14 @@ export interface CommentMatch { } export interface Change { - range: vscode.Range; + range: Range; text: string; } export interface Line { text: string; indentation: string; - range: vscode.Range; + range: Range; } export interface Token { diff --git a/src/commands/formatComments/utils.ts b/src/commands/formatComments/utils.ts index 8c76cd8..f437570 100644 --- a/src/commands/formatComments/utils.ts +++ b/src/commands/formatComments/utils.ts @@ -1,3 +1,5 @@ +import type { Selection, TextDocument } from "vscode"; +import { Range } from "vscode"; import type { Token } from "./types"; export const isValidLineRegex = /\w/; @@ -49,3 +51,39 @@ function joinLine(parts: string[], indentation: string, linePrefix: string): str } return text.length > 0 ? `${indentation}${linePrefix} ${text}` : `${indentation}${linePrefix}`; } + +export function matchAll( + document: TextDocument, + selections: readonly Selection[] | undefined, + regex: RegExp, + callback: (match: RegExpExecArray, range: Range) => void +) { + // Ranges are always the full line. We don't format parts of a comment. + const ranges = selections?.map((selection) => { + if (selection.isSingleLine) { + return document.lineAt(selection.start.line).range; + } + return new Range( + selection.start.with(undefined, 0), + document.lineAt(selection.end.line).range.end + ); + }); + + for (const range of ranges ?? [undefined]) { + const offset = range != null ? document.offsetAt(range.start) : 0; + const matches = document.getText(range).matchAll(regex); + + for (const match of matches) { + if (match.index == null) { + continue; + } + callback( + match as RegExpExecArray, + new Range( + document.positionAt(offset + match.index), + document.positionAt(offset + match.index + match[0].length) + ) + ); + } + } +} diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index e1b7f0a..f8961e1 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -14,7 +14,7 @@ import { newFile } from "./files/newFile"; import { removeFile } from "./files/removeFile"; import { renameFile } from "./files/renameFile"; import { focusTab } from "./focusTab"; -import { formatComments } from "./formatComments/formatComments"; +import { formatAllComments, formatComments } from "./formatComments/formatComments"; import { formatSelectedFiles, formatWorkspaceFiles } from "./formatFiles"; import { generateRange } from "./generateRange"; import { goToLine } from "./goToLine"; @@ -50,6 +50,7 @@ export function registerCommands( increment, decrement, formatComments, + formatAllComments, // Navigation openEditorAtIndex, focusTab, diff --git a/src/test/formatComments.test.ts b/src/test/formatComments.test.ts index e7c651e..4905546 100644 --- a/src/test/formatComments.test.ts +++ b/src/test/formatComments.test.ts @@ -1,5 +1,6 @@ import { commands } from "vscode"; import { runTest } from "./testUtil/runTest"; +import type { NumberSelection } from "./testUtil/test.types"; type Content = string | string[]; @@ -7,6 +8,8 @@ interface Test { title: string; pre: Content; post: Content; + preSelections?: NumberSelection; + postSelections?: NumberSelection; } interface Language { @@ -64,7 +67,17 @@ const templateLineTests: Test[] = [ } ]; -const pythonLineTests = templateLineTests; +const pythonLineTests: Test[] = [ + ...templateLineTests, + { + title: "Line | Selection", + pre: "# a\n# b\n# c", + post: "# a b\n# c", + preSelections: [0, 0, 1, 0], + postSelections: [0, 0, 0, 4] + } +]; + const cLineTests = createLineTests("//"); const luaLineTests = createLineTests("--"); @@ -166,18 +179,25 @@ const languages: Language[] = [ ) ]; -suite("Comment formatter", () => { +suite("Format comments", () => { for (const language of languages) { for (const fixture of language.tests) { runTest({ title: `${language.id} | ${fixture.title}`, - callback: () => commands.executeCommand("andreas.formatComments"), + callback: () => + commands.executeCommand( + fixture.preSelections + ? "andreas.formatComments" + : "andreas.formatAllComments" + ), pre: { language: language.id, - content: getContentString(fixture.pre) + content: getContentString(fixture.pre), + selections: fixture.preSelections }, post: { - content: getContentString(fixture.post) + content: getContentString(fixture.post), + selections: fixture.postSelections } }); }