From d5691e1d18ba116618020dc10e0a87cf938e54f1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:54:31 -0600 Subject: [PATCH 1/2] Fix: Preserve trailing commented-out code at end of files (#2) * Initial plan * Initial analysis: identify root cause of commented-out code deletion Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> * Fix: preserve trailing comments at end of file Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> * Address code review comments: add documentation and refactor logic Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> * Update src/tsco-cli/source-code/source-code-printer.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package-lock.json | 4 +-- .../source-code/source-code-analyzer.ts | 31 +++++++++++++++++++ .../source-code/source-code-organizer.ts | 3 +- .../source-code/source-code-printer.ts | 16 ++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c69894..feb48e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tsco", - "version": "2.0.7", + "version": "2.0.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "tsco", - "version": "2.0.7", + "version": "2.0.15", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", diff --git a/src/tsco-cli/source-code/source-code-analyzer.ts b/src/tsco-cli/source-code/source-code-analyzer.ts index 32e530a..bdb1867 100644 --- a/src/tsco-cli/source-code/source-code-analyzer.ts +++ b/src/tsco-cli/source-code/source-code-analyzer.ts @@ -29,6 +29,37 @@ export class SourceCodeAnalyzer return elements; } + /** + * Extracts trailing comments at the end of the file. + * These are comments that appear after the last syntax node and are attached to the EndOfFileToken. + * This ensures commented-out code at the end of files is preserved during reorganization. + * + * @param sourceFile The TypeScript source file to analyze + * @returns The trailing comments text, or null if there are no trailing comments + */ + public static getFileTrailer(sourceFile: ts.SourceFile) + { + // Get trailing comments from the EndOfFileToken + const children = sourceFile.getChildren(sourceFile); + const eofToken = children.find(node => node.kind === ts.SyntaxKind.EndOfFileToken); + + if (eofToken) + { + const fullText = sourceFile.getFullText(); + const commentRanges = ts.getLeadingCommentRanges(fullText, eofToken.pos); + + if (commentRanges && commentRanges.length > 0) + { + const start = commentRanges[0].pos; + const end = commentRanges[commentRanges.length - 1].end; + + return fullText.substring(start, end); + } + } + + return null; + } + public static hasReference(sourceFile: ts.SourceFile, identifier: string) { return sourceFile.getChildren(sourceFile).some(node => this.findReference(node, sourceFile, identifier)); diff --git a/src/tsco-cli/source-code/source-code-organizer.ts b/src/tsco-cli/source-code/source-code-organizer.ts index c0f71e8..a6965fc 100644 --- a/src/tsco-cli/source-code/source-code-organizer.ts +++ b/src/tsco-cli/source-code/source-code-organizer.ts @@ -40,9 +40,10 @@ export class SourceCodeOrganizer const sourceFile = ts.createSourceFile(sourceCodeFilePath, sourceCodeWithoutRegions.toString(), ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); const elements = SourceCodeAnalyzer.getNodes(sourceFile, configuration); + const fileTrailer = SourceCodeAnalyzer.getFileTrailer(sourceFile); const topLevelGroups = await this.organizeModuleMembers(elements, configuration, sourceFile, sourceCodeFilePath); // TODO: move this to module node - return SourceCodePrinter.print(fileHeader, topLevelGroups, configuration).toString(); + return SourceCodePrinter.print(fileHeader, topLevelGroups, fileTrailer, configuration).toString(); } catch (error) { diff --git a/src/tsco-cli/source-code/source-code-printer.ts b/src/tsco-cli/source-code/source-code-printer.ts index 3981b41..c08d192 100644 --- a/src/tsco-cli/source-code/source-code-printer.ts +++ b/src/tsco-cli/source-code/source-code-printer.ts @@ -30,7 +30,7 @@ export class SourceCodePrinter { // #region Public Static Methods (1) - public static print(fileHeader: string | null, nodeGroups: ElementNodeGroup[], configuration: Configuration) + public static print(fileHeader: string | null, nodeGroups: ElementNodeGroup[], fileTrailer: string | null, configuration: Configuration) { const printedSourceCode = this.printNodeGroups(nodeGroups, configuration); @@ -42,9 +42,21 @@ export class SourceCodePrinter printedSourceCode.removeConsecutiveEmptyLines(); printedSourceCode.trim(); - if (printedSourceCode.length > 0) + const hasContent = printedSourceCode.length > 0; + const hasTrailer = fileTrailer && fileTrailer.length > 0; + + if (hasContent) { printedSourceCode.addNewLineAfter(); + if (hasTrailer) + { + printedSourceCode.addNewLineAfter(); + } + } + + if (hasTrailer) + { + printedSourceCode.addAfter(fileTrailer); } return printedSourceCode; From e3b9ec9a56fb8ebcc6b176810a7afb4e6dabb09f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:05:21 -0600 Subject: [PATCH 2/2] Fix auto-save on focus change not triggering cleanup (#4) * Initial plan * Fix auto-save on focus change by using event.waitUntil() API * Fix pre-commit hook to use correct commands Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> * Fix incomplete sanitization vulnerability by using replaceAll Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> * Update src/extension.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor: Extract duplicate pattern matching logic and fix event.waitUntil() timing Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Christopher-C-Robinson <78235938+Christopher-C-Robinson@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .husky/pre-commit | 6 +- src/extension.ts | 145 ++++++++++++++++++++++++++++------------------ 2 files changed, 91 insertions(+), 60 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 02836f1..02bbb6e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,3 @@ -# organize code - -tsco --organize - # format code -eslint +npx eslint diff --git a/src/extension.ts b/src/extension.ts index 6793b20..0b7fcc4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import { fileExists, getDirectoryPath, getFullPath, getRelativePath, joinPath, r import { log, setLogger } from "./tsco-cli/source-code/source-code-logger"; import { SourceCodeOrganizer } from "./tsco-cli/source-code/source-code-organizer"; -// #region Functions (10) +// #region Functions (11) async function getConfiguration(configurationFilePath: string | null) { @@ -79,6 +79,34 @@ function matches(pattern: string, text: string) return globToRegExp(pattern).test(text); } +function shouldOrganizeFile(sourceCodeFilePathRelative: string, configuration: Configuration): { shouldOrganize: boolean, reason?: string } +{ + let include = true; + let exclude = false; + + if (configuration.files.include.length > 0) + { + include = configuration.files.include.some(inc => matches(inc, sourceCodeFilePathRelative) || matches(inc, sourceCodeFilePathRelative.replaceAll("../", "").replaceAll("./", ""))); + } + + if (configuration.files.exclude.length > 0) + { + exclude = configuration.files.exclude.some(exc => matches(exc, sourceCodeFilePathRelative) || matches(exc, sourceCodeFilePathRelative.replaceAll("../", "").replaceAll("./", ""))); + } + + if (!include) + { + return { shouldOrganize: false, reason: "does not match file include patterns" }; + } + + if (exclude) + { + return { shouldOrganize: false, reason: "matches file exclude patterns" }; + } + + return { shouldOrganize: true }; +} + async function onInitialize() { if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) @@ -144,22 +172,47 @@ async function onOrganizeAll() } } -async function onSave(sourceCodeFilePath: string) +async function onSave(event: vscode.TextDocumentWillSaveEvent) { - if (settings.organizeOnSave) + if (settings.organizeOnSave && event.document.languageId === "typescript") { - const editor = vscode.window.visibleTextEditors.find(ed => ed.document.uri.fsPath === sourceCodeFilePath); - - if (editor) + const sourceCodeFilePath = getFullPath(event.document.uri.fsPath); + + if (matches("**/*.ts", sourceCodeFilePath)) { - if (await onOrganize(sourceCodeFilePath)) - { - savingHandler.dispose(); - - await editor.document.save(); - - savingHandler = vscode.workspace.onWillSaveTextDocument(async (e) => await onSave(e.document.uri.fsPath)); - } + event.waitUntil((async () => { + const configuration = await getConfiguration(settings.configurationFilePath); + const workspaceRootDirectoryPath = getWorkspaceRootDirectoryPath(); + const sourceCodeDirectoryPath = workspaceRootDirectoryPath; + const sourceCodeFilePathRelative = getRelativePath(sourceCodeDirectoryPath, sourceCodeFilePath); + + const fileCheck = shouldOrganizeFile(sourceCodeFilePathRelative, configuration); + + if (!fileCheck.shouldOrganize) + { + log(`tsco skipping organizing ${sourceCodeFilePath}, because it ${fileCheck.reason}`); + return []; + } + + const sourceCode = event.document.getText(); + const organizedSourceCode = await SourceCodeOrganizer.organizeSourceCode(sourceCodeFilePath, sourceCode, configuration); + + if (organizedSourceCode !== sourceCode) + { + const start = new vscode.Position(0, 0); + const end = new vscode.Position(event.document.lineCount - 1, event.document.lineAt(event.document.lineCount - 1).text.length); + const range = new vscode.Range(start, end); + + log(`tsco organized ${sourceCodeFilePath}`); + + return [vscode.TextEdit.replace(range, organizedSourceCode)]; + } + else + { + log(`tsco skipping organizing ${sourceCodeFilePath}, because it is already organized`); + return []; + } + })()); } } } @@ -184,55 +237,38 @@ async function organize(sourceCodeFilePath: string, configuration: Configuration const sourceCodeDirectoryPath = workspaceRootDirectoryPath; const sourceCodeFilePathRelative = getRelativePath(sourceCodeDirectoryPath, sourceCodeFilePath); - // test for include or exclude patterns - let include = true; - let exclude = false; - - if (configuration.files.include.length > 0) + const fileCheck = shouldOrganizeFile(sourceCodeFilePathRelative, configuration); + + if (!fileCheck.shouldOrganize) { - include = configuration.files.include.some(inc => matches(inc, sourceCodeFilePathRelative) || matches(inc, sourceCodeFilePathRelative.replace("../", "").replace("./", ""))); + log(`tsco skipping organizing ${sourceCodeFilePath}, because it ${fileCheck.reason}`); + return false; } - if (configuration.files.exclude.length > 0) - { - exclude = configuration.files.exclude.some(exc => matches(exc, sourceCodeFilePathRelative) || matches(exc, sourceCodeFilePathRelative.replace("../", "").replace("./", ""))); - } + // organize and save + let editor = await getOpenedEditor(sourceCodeFilePath); + const sourceCode = editor ? editor.document.getText() : await readFile(sourceCodeFilePath); + const organizedSourceCode = await SourceCodeOrganizer.organizeSourceCode(sourceCodeFilePath, sourceCode, configuration); - if (include && !exclude) + if (organizedSourceCode !== sourceCode) { - // organize and save - let editor = await getOpenedEditor(sourceCodeFilePath); - const sourceCode = editor ? editor.document.getText() : await readFile(sourceCodeFilePath); - const organizedSourceCode = await SourceCodeOrganizer.organizeSourceCode(sourceCodeFilePath, sourceCode, configuration); + editor ??= await openEditor(sourceCodeFilePath); + const start = new vscode.Position(0, 0); + const end = new vscode.Position(editor.document.lineCount - 1, editor.document.lineAt(editor.document.lineCount - 1).text.length); + const range = new vscode.Range(start, end); + const edit = new vscode.WorkspaceEdit(); - if (organizedSourceCode !== sourceCode) - { - editor ??= await openEditor(sourceCodeFilePath); - const start = new vscode.Position(0, 0); - const end = new vscode.Position(editor.document.lineCount, editor.document.lineAt(editor.document.lineCount - 1).text.length); - const range = new vscode.Range(start, end); - const edit = new vscode.WorkspaceEdit(); - - edit.replace(editor.document.uri, range, organizedSourceCode); + edit.replace(editor.document.uri, range, organizedSourceCode); - await vscode.workspace.applyEdit(edit); + await vscode.workspace.applyEdit(edit); - log(`tsco organized ${sourceCodeFilePath}`); + log(`tsco organized ${sourceCodeFilePath}`); - return true; - } - else - { - log(`tsco skipping organizing ${sourceCodeFilePath}, because it is already organized`); - } - } - else if (!include) - { - log(`tsco skipping organizing ${sourceCodeFilePath}, because it does not match file include patterns`); + return true; } - else if (exclude) + else { - log(`tsco skipping organizing ${sourceCodeFilePath}, because it matches file exclude patterns`); + log(`tsco skipping organizing ${sourceCodeFilePath}, because it is already organized`); } return false; @@ -249,7 +285,7 @@ export function activate(context: vscode.ExtensionContext) context.subscriptions.push(vscode.commands.registerCommand('tsco.organizeAll', async () => await onOrganizeAll())); vscode.workspace.onDidChangeConfiguration(() => settings = Settings.getSettings()); - savingHandler = vscode.workspace.onWillSaveTextDocument(async (e) => await onSave(e.document.uri.fsPath)); + context.subscriptions.push(vscode.workspace.onWillSaveTextDocument((e) => onSave(e))); setLogger({ log: (message: string) => output.appendLine(message), @@ -259,11 +295,10 @@ export function activate(context: vscode.ExtensionContext) // #endregion Exported Functions -// #region Variables (3) +// #region Variables (2) const output = vscode.window.createOutputChannel("tsco"); -let savingHandler: vscode.Disposable; let settings = Settings.getSettings(); // #endregion Variables