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
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.

13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
"onDidSaveTextDocument"
],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "SortMyFiles",
"properties": {
"sortmyfiles.configMode": {
"type": "string",
"default": ".order",
"enum": [".order", "Hugo"],
"description": "Determines whether to use '.order' file or Hugo weight system for sorting"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
Expand Down
49 changes: 46 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
import { getProjectPath, prefixWithProjectPath } from './projectPath';
import { readFileSync, appendFileSync, utimesSync } from 'fs';
import { outputChannel } from './logging';
import * as vscode from 'vscode';

let configName = '.order';
export let regularExpressionTag = "(regex)";

/**
* Enum for all supported configuration modes
*/
export enum ConfigMode {
ORDER = '.order',
HUGO = 'Hugo'
}

export class InvalidConfigModeError extends Error {
constructor(public modeName: string) {
super(`Invalid config mode: ${modeName}`);
this.name = "InvalidConfigModeError";
}
}

/**
* Gets the current configuration mode from settings
* @returns The current configuration mode
*/
export function getConfigMode(): ConfigMode {
const config = vscode.workspace.getConfiguration('sortmyfiles');
const configModeString = config.get<string>('configMode', ConfigMode.ORDER);
if (Object.values(ConfigMode).includes(configModeString as ConfigMode)) {
return configModeString as ConfigMode;
}
throw new InvalidConfigModeError(configModeString || 'undefined');
}

function configReader(): string[] {
let customOrderPath = getProjectPath() + configName;
// Only read from file in .order mode
let configMode = getConfigMode();

if (configMode === ConfigMode.HUGO) {
// In Hugo mode, we don't read a config file
return [];
}

let customOrderPath = getProjectPath() + configMode;
let fileContent = readFileSync(customOrderPath, 'utf-8');
let lines = fileContent.split(/\r?\n/); // Handles both Windows and Unix line endings
let removedEmptyLines = lines.filter(item => item !== '');
Expand All @@ -24,14 +60,21 @@ export function getRegexLines(lines: string[]): string[] {
export function getConfig(): string[] {
// Public function with exception handling
let fileOrder: string[];

// If in Hugo mode, don't try to read a config file
if (getConfigMode() === ConfigMode.HUGO) {
return [];
}

try {
fileOrder = configReader();
return fileOrder;
} catch (error) {
if (error instanceof URIError) {
outputChannel.appendLine("Workspace path not detected. Please open a workspace.");
} else if (error instanceof Error && error.message.includes('ENOENT')) {
outputChannel.appendLine(`Config file "${configName}" not found.`);
const configMode = getConfigMode();
outputChannel.appendLine(`Config file "${configMode}" not found.`);
} else if (error instanceof Error) {
outputChannel.appendLine(`Failed to load configuration: ${error.message}`);
} else {
Expand Down
49 changes: 36 additions & 13 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { readFileSync, appendFileSync, utimesSync } from 'fs';
import { getProjectPath, prefixWithProjectPath } from './projectPath';
import { findAllFilesAndFoldersWithIgnore, getGitignoreFiles } from './findFiles';
import { outputChannel } from './logging';
import { getConfig, getRegexLines, regularExpressionTag } from './config';
import { getConfig, getRegexLines, regularExpressionTag, getConfigMode, ConfigMode } from './config';
import { alpahabeticalllySortFiles } from './sortingFunctions';
import { hugoSortFiles } from './hugoSorting';

function modifyLastChangedDateForFiles(fileList: string[]) {
let milliseconds = 0;
Expand All @@ -29,16 +30,27 @@ function changeDefaultSortOrder(newValue: string) {
}

async function sortFiles() {
let fileOrder = getConfig();
if (fileOrder.length === 0) {
// if config does not exist do nothing
return;
// Get current config mode
const configMode = getConfigMode();

if (configMode === ConfigMode.ORDER) {
// In .order mode, we need a config file
let fileOrder = getConfig();
if (fileOrder.length === 0) {
// if config does not exist in .order mode, do nothing
return;
}
fileOrder = fileOrder.filter(line => !line.startsWith(regularExpressionTag));
let prefixedFileOrder = prefixWithProjectPath(fileOrder);
let sortedNonConfigFiles = await getNonConfigFilesSorted();
let combinedList = [...sortedNonConfigFiles, ...prefixedFileOrder];
modifyLastChangedDateForFiles(combinedList);
} else if (configMode === ConfigMode.HUGO) {
// In Hugo mode, we just sort all files using the Hugo sorting algorithm
let sortedFiles = await getNonConfigFilesSorted();
modifyLastChangedDateForFiles(sortedFiles);
}
fileOrder = fileOrder.filter(line => !line.startsWith(regularExpressionTag));
let prefixedFileOrder = prefixWithProjectPath(fileOrder);
let sortedNonConfigFiles = await getNonConfigFilesSorted();
let combinedList = [...sortedNonConfigFiles, ...prefixedFileOrder];
modifyLastChangedDateForFiles(combinedList);

outputChannel.appendLine("Sorting completed");
}

Expand All @@ -58,9 +70,20 @@ async function getNonConfigFilesSorted(): Promise<string[]> {

await findAllFilesAndFoldersWithIgnore(workspaceUri, filesAndFolders, ignorePattern);
let nonConfigFilesAndFolders = Array.from(filesAndFolders).filter(name => !config.includes(name));
let alpahabeticalllySorted = alpahabeticalllySortFiles(nonConfigFilesAndFolders);
let regexSorted = putFilesFitsToRegexPatternToEnd(alpahabeticalllySorted, getRegexLines(config));
return regexSorted;

// Use different sorting strategies based on the selected mode
const configMode = getConfigMode();

if (configMode === ConfigMode.HUGO) {
// In Hugo mode, use the Hugo-specific sorting algorithm
outputChannel.appendLine("Using Hugo mode sorting");
return hugoSortFiles(nonConfigFilesAndFolders);
} else {
// In .order mode, use the original sorting algorithm
let alpahabeticalllySorted = alpahabeticalllySortFiles(nonConfigFilesAndFolders);
let regexSorted = putFilesFitsToRegexPatternToEnd(alpahabeticalllySorted, getRegexLines(config));
return regexSorted;
}
}


Expand Down
172 changes: 172 additions & 0 deletions src/hugoSorting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as fs from 'fs';
import * as path from 'path';
import { outputChannel } from './logging';

interface FileMetadata {
path: string;
isDirectory: boolean;
weight: number | null;
isIndexFile: boolean;
}

/**
* Extract YAML frontmatter from a markdown file and get the weight value.
* @param filePath Path to the markdown file
* @returns The weight value if found, otherwise null
*/
function extractWeightFromFrontmatter(filePath: string): number | null {
try {
const content = fs.readFileSync(filePath, 'utf-8');

// Check if file has YAML frontmatter (between --- markers)
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
return null;
}

const frontmatter = frontmatterMatch[1];

// Look for weight: value in the frontmatter
const weightMatch = frontmatter.match(/weight\s*:\s*(\d+)/);
if (weightMatch && weightMatch[1]) {
return parseInt(weightMatch[1], 10);
}

return null;
} catch (error) {
outputChannel.appendLine(`Error reading weight from ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
return null;
}
}

/**
* Get the weight of a directory by looking for _index.md file inside it.
* @param dirPath Path to the directory
* @returns The weight from _index.md if found, otherwise null
*/
function getDirectoryWeight(dirPath: string): number | null {
const indexPath = path.join(dirPath, '_index.md');

if (fs.existsSync(indexPath)) {
return extractWeightFromFrontmatter(indexPath);
}

return null;
}

/**
* Get metadata for a file or directory, including weight information.
* @param filePath Path to the file or directory
* @returns Metadata object with path, type, and weight information
*/
function getFileMetadata(filePath: string): FileMetadata {
const stats = fs.statSync(filePath);
const isDirectory = stats.isDirectory();
const filename = path.basename(filePath);
const isIndexFile = filename === '_index.md';

let weight: number | null = null;

if (isDirectory) {
weight = getDirectoryWeight(filePath);
} else if (path.extname(filePath) === '.md') {
weight = extractWeightFromFrontmatter(filePath);
}

return {
path: filePath,
isDirectory,
weight,
isIndexFile
};
}

/**
* Sort files according to Hugo conventions:
* 1. _index.md files first within their directory
* 2. Files with weight sorted by weight
* 3. Everything else sorted alphabetically
* @param files Array of file paths to sort
* @returns Sorted array of file paths
*/
export function hugoSortFiles(files: string[]): string[] {
// Get metadata for all files
const fileMetadata = files.map(getFileMetadata);

// Group files by directory to ensure _index.md is at the top of its own directory
const filesByDirectory = new Map<string, FileMetadata[]>();

fileMetadata.forEach(metadata => {
const dirPath = path.dirname(metadata.path);
if (!filesByDirectory.has(dirPath)) {
filesByDirectory.set(dirPath, []);
}
filesByDirectory.get(dirPath)!.push(metadata);
});

// Sort files within each directory
for (const [dirPath, files] of filesByDirectory.entries()) {
files.sort((a, b) => {
// Rule 1: _index.md files always at the top of its directory
// Since we're reversing other sorts for VSCode, we need to reverse this logic too
if (a.isIndexFile && !b.isIndexFile) {
return 1; // Reversed to put _index.md at top with VSCode's sorting
}
if (!a.isIndexFile && b.isIndexFile) {
return -1; // Reversed to put _index.md at top with VSCode's sorting
}

// Rule 2: Files with weight sorted by weight (lower numbers first, but reverse for VSCode)
if (a.weight !== null && b.weight !== null) {
return b.weight - a.weight;
}

// Files with weight come before files without weight (reverse for VSCode)
if (a.weight !== null && b.weight === null) {
return 1;
}
if (a.weight === null && b.weight !== null) {
return -1;
}

// Rule 3: Alphabetical sorting for ties (reverse for VSCode)
return b.path.localeCompare(a.path);
});
}

// Now sort directories based on their _index.md weight if available
const sortedDirs = Array.from(filesByDirectory.keys()).sort((dirA, dirB) => {
// Special case for the root directory, which should come first
if (dirA === '.' || dirA === '/') return -1;
if (dirB === '.' || dirB === '/') return 1;

// Try to get the weight of each directory from its _index.md
const dirAWeight = getDirectoryWeight(dirA);
const dirBWeight = getDirectoryWeight(dirB);

// If both directories have a weight, sort by weight (reverse for VSCode)
if (dirAWeight !== null && dirBWeight !== null) {
return dirBWeight - dirAWeight;
}

// Directories with weight come before directories without weight (reverse for VSCode)
if (dirAWeight !== null && dirBWeight === null) {
return 1;
}
if (dirAWeight === null && dirBWeight !== null) {
return -1;
}

// Default to alphabetical sorting (reverse for VSCode)
return dirB.localeCompare(dirA);
});

// Collect all sorted files from all directories
const result: string[] = [];

for (const dir of sortedDirs) {
result.push(...filesByDirectory.get(dir)!.map(metadata => metadata.path));
}

return result;
}