diff --git a/package-lock.json b/package-lock.json index fd33e7c..14a2c79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "SortMyFiles", - "version": "0.1.3", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "SortMyFiles", - "version": "0.1.3", + "version": "0.1.7", "dependencies": { "minimatch": "^10.0.1" }, diff --git a/package.json b/package.json index c92a75c..178b8db 100644 --- a/package.json +++ b/package.json @@ -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 ./", diff --git a/src/config.ts b/src/config.ts index 14838fc..2e51dd4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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('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 !== ''); @@ -24,6 +60,12 @@ 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; @@ -31,7 +73,8 @@ export function getConfig(): string[] { 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 { diff --git a/src/extension.ts b/src/extension.ts index d1875bd..94a6ebf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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; @@ -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"); } @@ -58,9 +70,20 @@ async function getNonConfigFilesSorted(): Promise { 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; + } } diff --git a/src/hugoSorting.ts b/src/hugoSorting.ts new file mode 100644 index 0000000..7b6d580 --- /dev/null +++ b/src/hugoSorting.ts @@ -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(); + + 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; +} \ No newline at end of file