Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ You can also access the settings by using the command `achievements.settings`.
|`achievements.listeners.tabs`|Enable or disable tab listeners|
|`achievements.listeners.tasks`|Enable or disable task listeners|
|`achievements.listeners.time`|Enable or disable time tracking listeners|
|`achievements.ignore.files`|List of file names ignored by file-based achievements (case-insensitive, for example `package-lock.json` matches `PACKAGE-LOCK.JSON`)|
|`achievements.ignore.directories`|List of directory names ignored by file-based achievements (case-sensitive, for example `.git` matches only `.git`)|

## Installation

Expand Down
29 changes: 29 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@
"type": "boolean",
"default": true,
"description": "Enable or disable time events listeners for the Achievements extension."
},
"achievements.ignore.files": {
"type": "array",
"default": [
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
".ds_store",
"thumbs.db"
],
"items": {
"type": "string"
},
"description": "File names ignored by file-related achievements (case-insensitive)."
},
"achievements.ignore.directories": {
"type": "array",
"default": [
".git",
".svn",
".hg",
".jj",
"node_modules"
],
"items": {
"type": "string"
},
"description": "Directory names ignored by file-related achievements (case-sensitive)."
}
}
}
Expand Down
29 changes: 24 additions & 5 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import fs from "fs";
import logger from "../utils/logger";
import * as vscode from "vscode";
import { webview } from "../views/viewconst";
import { constants } from "../constants";

// ==================== TYPES ====================
// Config interface, defines the structure of the config object, always json serializable
Expand All @@ -19,6 +20,10 @@ export interface Config {
notifications: boolean;
logDirectory: string;
username: string;
ignore: {
files: string[];
directories: string[];
};
listeners: {
debug: boolean;
extensions: boolean;
Expand Down Expand Up @@ -82,7 +87,7 @@ export namespace config {
vscode.window
.showInformationMessage(
"For a better experience, please set your username",
"Set Username"
"Set Username",
)
.then((selection) => {
if (selection === "Set Username") {
Expand Down Expand Up @@ -125,11 +130,25 @@ export namespace config {
username: extensionRawConfig
.get<string>("username", webview.DEFAULT_USER)
.trim(),
ignore: {
files: extensionRawConfig
.get<string[]>("ignore.files", [...constants.ignore.DEFAULT_FILES])
.filter((name) => typeof name === "string")
.map((name) => name.trim())
.filter((name) => name.length > 0),
directories: extensionRawConfig
.get<string[]>("ignore.directories", [
...constants.ignore.DEFAULT_DIRECTORIES,
])
.filter((name) => typeof name === "string")
.map((name) => name.trim())
.filter((name) => name.length > 0),
},
listeners: {
debug: extensionRawConfig.get<boolean>("listeners.debug", true),
extensions: extensionRawConfig.get<boolean>(
"listeners.extensions",
true
true,
),
files: extensionRawConfig.get<boolean>("listeners.files", true),
git: extensionRawConfig.get<boolean>("listeners.git", true),
Expand All @@ -142,7 +161,7 @@ export namespace config {
// Check if the log directory is a valid path, if not, set it to the default
if (!path.isAbsolute(extensionConfig.logDirectory)) {
logger.warn(
`Invalid log directory path: ${extensionConfig.logDirectory}, setting to default`
`Invalid log directory path: ${extensionConfig.logDirectory}, setting to default`,
);
updateConfig("logDirectory", defaultLogDir);
extensionConfig.logDirectory = defaultLogDir;
Expand All @@ -151,7 +170,7 @@ export namespace config {
fs.mkdirSync(extensionConfig.logDirectory, { recursive: true });
} catch (error) {
logger.error(
`Error creating log directory: ${error}, setting to default`
`Error creating log directory: ${error}, setting to default`,
);
updateConfig("logDirectory", defaultLogDir);
extensionConfig.logDirectory = defaultLogDir;
Expand Down Expand Up @@ -232,7 +251,7 @@ export namespace config {
}

export function isListenerEnabled(
listener: keyof Config["listeners"]
listener: keyof Config["listeners"],
): boolean {
const listeners = getConfig().listeners;

Expand Down
12 changes: 12 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,16 @@ export namespace constants {
TASKS: "tasks",
TIME: "time",
} as const;

export const ignore = {
DEFAULT_FILES: [
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
".ds_store",
"thumbs.db",
],
DEFAULT_DIRECTORIES: [".git", ".svn", ".hg", ".jj", "node_modules"],
} as const;
}
127 changes: 109 additions & 18 deletions src/listeners/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,57 @@ import path from "node:path";
import logger from "../utils/logger";
import { config } from "../config/config";

let ignoredDirectoryNames = new Set<string>();
let ignoredFileNames = new Set<string>();

function refreshIgnoredMatchers(): void {
const extensionRawConfig = vscode.workspace.getConfiguration("achievements");

ignoredDirectoryNames = new Set(
extensionRawConfig
.get<string[]>("ignore.directories", [
...constants.ignore.DEFAULT_DIRECTORIES,
])
.filter((name) => typeof name === "string")
.map((name) => name.trim())
.filter((name) => name.length > 0),
);

ignoredFileNames = new Set(
extensionRawConfig
.get<string[]>("ignore.files", [...constants.ignore.DEFAULT_FILES])
.filter((name) => typeof name === "string")
.map((name) => name.trim().toLowerCase())
.filter((name) => name.length > 0),
);
Comment thread
BoxBoxJason marked this conversation as resolved.
}

/**
* Check if a given URI should be ignored for progression counting based on its path and file name.
*
* @param {vscode.Uri} uri - The URI to check
* @returns {boolean} True if the URI should be ignored, false otherwise
*/
function shouldIgnoreUri(uri: vscode.Uri): boolean {
if (uri.scheme !== "file") {
return true;
}

if (ignoredDirectoryNames.size === 0 && ignoredFileNames.size === 0) {
refreshIgnoredMatchers();
}

const normalizedPath = path.normalize(uri.fsPath);
const segments = normalizedPath.split(path.sep);

if (segments.some((segment) => ignoredDirectoryNames.has(segment))) {
return true;
}

const fileName = path.basename(normalizedPath).toLowerCase();
return ignoredFileNames.has(fileName);
}

/**
* File related events listeners functions and handlers
*
Expand All @@ -31,41 +82,56 @@ export namespace fileListeners {
if (config.isListenerEnabled(constants.listeners.FILES)) {
logger.info("Starting file events listeners");

refreshIgnoredMatchers();

// Watcher for resources
const resourcesWatcher = vscode.workspace.createFileSystemWatcher(
"**/*",
false,
false,
false
false,
);

resourcesWatcher.onDidCreate(
handleCreateEvent,
null,
context.subscriptions
context.subscriptions,
);
resourcesWatcher.onDidDelete(
handleDeleteEvent,
null,
context.subscriptions
context.subscriptions,
);
vscode.workspace.onDidRenameFiles(
handleRenameEvent,
null,
context.subscriptions
context.subscriptions,
);

// Text document changes
vscode.workspace.onDidChangeTextDocument(
handleTextChangedEvent,
null,
context.subscriptions
context.subscriptions,
);
// Diagnostics changes
vscode.languages.onDidChangeDiagnostics(
handleDiagnosticChangedEvent,
null,
context.subscriptions
context.subscriptions,
);

vscode.workspace.onDidChangeConfiguration(
(event) => {
if (
event.affectsConfiguration("achievements.ignore.files") ||
event.affectsConfiguration("achievements.ignore.directories")
) {
refreshIgnoredMatchers();
}
},
null,
context.subscriptions,
);

logger.debug("File listeners activated");
Expand All @@ -83,11 +149,15 @@ export namespace fileListeners {
* @returns {Promise<void>}
*/
export async function handleCreateEvent(uri: vscode.Uri): Promise<void> {
if (shouldIgnoreUri(uri)) {
return;
}

const stats = await vscode.workspace.fs.stat(uri);
if (stats.type === vscode.FileType.File) {
// Increase file created count
await ProgressionController.increaseProgression(
constants.criteria.FILES_CREATED
constants.criteria.FILES_CREATED,
);

// Retrieve file extension
Expand All @@ -102,7 +172,7 @@ export namespace fileListeners {
} else if (stats.type === vscode.FileType.Directory) {
// Increase directory created count
await ProgressionController.increaseProgression(
constants.criteria.DIRECTORY_CREATED
constants.criteria.DIRECTORY_CREATED,
);
}
}
Expand All @@ -116,8 +186,12 @@ export namespace fileListeners {
* @returns {Promise<void>}
*/
export async function handleDeleteEvent(uri: vscode.Uri): Promise<void> {
if (shouldIgnoreUri(uri)) {
return;
}

await ProgressionController.increaseProgression(
constants.criteria.RESOURCE_DELETED
constants.criteria.RESOURCE_DELETED,
);
}

Expand All @@ -130,11 +204,20 @@ export namespace fileListeners {
* @returns {Promise<void>}
*/
export async function handleRenameEvent(
event: vscode.FileRenameEvent
event: vscode.FileRenameEvent,
): Promise<void> {
const relevantFileCount = event.files.filter(
({ oldUri, newUri }) =>
!shouldIgnoreUri(oldUri) && !shouldIgnoreUri(newUri),
).length;

if (relevantFileCount === 0) {
return;
}

await ProgressionController.increaseProgression(
constants.criteria.FILES_RENAMED,
event.files.length
relevantFileCount,
);
}

Expand All @@ -147,8 +230,12 @@ export namespace fileListeners {
* @returns {Promise<void>}
*/
export async function handleTextChangedEvent(
event: vscode.TextDocumentChangeEvent
event: vscode.TextDocumentChangeEvent,
): Promise<void> {
if (shouldIgnoreUri(event.document.uri)) {
return;
}

const language =
constants.labels.LANGUAGES_EXTENSIONS[
path.extname(event.document.fileName)
Expand All @@ -163,19 +250,19 @@ export namespace fileListeners {
// Increment progression for the added lines
await ProgressionController.increaseProgression(
constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language),
totalLinesAdded
totalLinesAdded,
);
// Increment generic lines of code progression
await ProgressionController.increaseProgression(
constants.criteria.LINES_OF_CODE,
totalLinesAdded
totalLinesAdded,
);
}
}
}

function countLinesAdded(
change: vscode.TextDocumentContentChangeEvent
change: vscode.TextDocumentContentChangeEvent,
): number {
// Check if the change involves adding new lines
if (change.text.includes("\n")) {
Expand All @@ -195,24 +282,28 @@ export namespace fileListeners {
let errorCounterFree = true;

export async function handleDiagnosticChangedEvent(
event: vscode.DiagnosticChangeEvent
event: vscode.DiagnosticChangeEvent,
): Promise<void> {
if (errorCounterFree) {
errorCounterFree = false;
for (const uri of event.uris) {
if (shouldIgnoreUri(uri)) {
continue;
}

Comment thread
BoxBoxJason marked this conversation as resolved.
const errorCount = vscode.languages
.getDiagnostics(uri)
.filter(
(diagnostic) =>
diagnostic.severity === vscode.DiagnosticSeverity.Error
diagnostic.severity === vscode.DiagnosticSeverity.Error,
).length;
const filePath = uri.fsPath;
const previousErrorCount = fileErrorCounts.get(filePath);
if (previousErrorCount !== undefined) {
if (errorCount < previousErrorCount) {
await ProgressionController.increaseProgression(
constants.criteria.ERRORS_FIXED,
previousErrorCount - errorCount
previousErrorCount - errorCount,
);
}
}
Expand Down
Loading
Loading