Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 1 addition & 5 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# organize code

tsco --organize

# format code

eslint
npx eslint
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 90 additions & 55 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 [];
}
})());
}
}
}
Expand All @@ -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;
Expand All @@ -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),
Expand All @@ -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
31 changes: 31 additions & 0 deletions src/tsco-cli/source-code/source-code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method should have an explicit return type annotation. Add : string | null to match the documented return type in the JSDoc.

Suggested change
public static getFileTrailer(sourceFile: ts.SourceFile)
public static getFileTrailer(sourceFile: ts.SourceFile): string | null

Copilot uses AI. Check for mistakes.
{
// 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));
Expand Down
3 changes: 2 additions & 1 deletion src/tsco-cli/source-code/source-code-organizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
16 changes: 14 additions & 2 deletions src/tsco-cli/source-code/source-code-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hasTrailer check is redundant because fileTrailer.length > 0 already returns false for null/undefined values due to short-circuit evaluation. Consider simplifying to const hasTrailer = !!fileTrailer && fileTrailer.length > 0; for clarity, or use optional chaining: const hasTrailer = (fileTrailer?.length ?? 0) > 0;.

Suggested change
const hasTrailer = fileTrailer && fileTrailer.length > 0;
const hasTrailer = (fileTrailer?.length ?? 0) > 0;

Copilot uses AI. Check for mistakes.

if (hasContent)
{
printedSourceCode.addNewLineAfter();
if (hasTrailer)
{
printedSourceCode.addNewLineAfter();
}
}

if (hasTrailer)
{
printedSourceCode.addAfter(fileTrailer);
}

return printedSourceCode;
Expand Down