Skip to content

Commit f115602

Browse files
committed
feat: track lines of comments (#50)
Signed-off-by: BoxBoxJason <contact@boxboxjason.dev>
1 parent 1de4453 commit f115602

4 files changed

Lines changed: 302 additions & 11 deletions

File tree

.vscode-test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
const { defineConfig } = require('@vscode/test-cli');
1+
const { defineConfig } = require("@vscode/test-cli");
22

33
module.exports = defineConfig([
44
{
5-
label: 'unitTests',
6-
files: 'dist/test/**/*.test.js',
7-
version: 'stable',
8-
workspaceFolder: '.',
5+
label: "unitTests",
6+
files: "dist/test/**/*.test.js",
7+
version: "stable",
8+
workspaceFolder: ".",
99
mocha: {
10-
ui: 'tdd',
11-
timeout: 20000
12-
}
13-
}
10+
ui: "tdd",
11+
timeout: 20000,
12+
},
13+
},
1414
]);

src/listeners/files.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ export namespace fileListeners {
242242
];
243243
if (language) {
244244
let totalLinesAdded = 0;
245+
let totalCommentLinesAdded = 0;
245246
for (const change of event.contentChanges) {
246247
totalLinesAdded += countLinesAdded(change);
248+
totalCommentLinesAdded += countCommentLinesAdded(change, event.document);
247249
}
248250

249251
if (totalLinesAdded > 0) {
@@ -258,6 +260,13 @@ export namespace fileListeners {
258260
totalLinesAdded,
259261
);
260262
}
263+
264+
if (totalCommentLinesAdded > 0) {
265+
await ProgressionController.increaseProgression(
266+
constants.criteria.LINES_OF_COMMENTS,
267+
totalCommentLinesAdded,
268+
);
269+
}
261270
}
262271
}
263272

@@ -278,6 +287,53 @@ export namespace fileListeners {
278287
return 0;
279288
}
280289

290+
function isCommentLine(line: string): boolean {
291+
const trimmed = line.trim();
292+
if (!trimmed) {
293+
return false;
294+
}
295+
if (trimmed.startsWith("//") || trimmed.startsWith("/*")) {
296+
return true;
297+
}
298+
// Block comment continuation/end: "* text" or "*/"
299+
if (
300+
trimmed[0] === "*" &&
301+
(trimmed.length === 1 || trimmed[1] === " " || trimmed[1] === "/")
302+
) {
303+
return true;
304+
}
305+
return (
306+
trimmed.startsWith("#") ||
307+
trimmed.startsWith("--") ||
308+
trimmed.startsWith("%") ||
309+
trimmed.startsWith("<!--") ||
310+
trimmed.startsWith("-->")
311+
);
312+
}
313+
314+
function countCommentLinesAdded(
315+
change: vscode.TextDocumentContentChangeEvent,
316+
document: vscode.TextDocument,
317+
): number {
318+
if (!change.text.includes("\n")) {
319+
return 0;
320+
}
321+
// When the added text is only whitespace (bare Enter, possibly with
322+
// auto-indentation), the comment content is NOT in change.text — it
323+
// was already in the document. Check the line the cursor was on.
324+
if (change.text.trim().length === 0) {
325+
return isCommentLine(
326+
document.lineAt(change.range.start.line).text,
327+
)
328+
? 1
329+
: 0;
330+
}
331+
// Multi-line paste / snippet: count comment lines in the inserted content.
332+
return change.text
333+
.split(/\r?\n/)
334+
.filter((line) => isCommentLine(line)).length;
335+
}
336+
281337
const fileErrorCounts = new Map<string, number>();
282338
let errorCounterFree = true;
283339

src/test/listeners.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ suite("Listeners Test Suite", () => {
103103
fileName: "test.ts",
104104
uri: vscode.Uri.file("test.ts"),
105105
languageId: "typescript",
106+
lineAt: (_line: number) => ({ text: "const x = 1;" }),
106107
},
107108
contentChanges: [
108109
{

src/test/listeners/files.test.ts

Lines changed: 236 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ suite("File Listeners Test Suite", () => {
214214
const document = {
215215
fileName: testFile,
216216
uri: uri,
217-
} as vscode.TextDocument;
217+
lineAt: (_line: number) => ({ text: "const x = 1;" }),
218+
} as unknown as vscode.TextDocument;
218219

219220
const event = {
220221
document: document,
@@ -233,7 +234,7 @@ suite("File Listeners Test Suite", () => {
233234
text: "line1\nline2\n", // 2 lines
234235
},
235236
],
236-
} as vscode.TextDocumentChangeEvent;
237+
} as unknown as vscode.TextDocumentChangeEvent;
237238

238239
let callCount = 0;
239240
let totalLines = 0;
@@ -261,6 +262,239 @@ suite("File Listeners Test Suite", () => {
261262
}
262263
});
263264

265+
test("handleTextChangedEvent should increase LINES_OF_COMMENTS for pasted comment lines", async () => {
266+
const testFile = path.join(tempDir, "test_comment.ts");
267+
const uri = vscode.Uri.file(testFile);
268+
const document = {
269+
fileName: testFile,
270+
uri: uri,
271+
} as vscode.TextDocument;
272+
273+
const event = {
274+
document: document,
275+
reason: undefined,
276+
contentChanges: [
277+
{
278+
range: new vscode.Range(0, 0, 0, 0),
279+
rangeOffset: 0,
280+
rangeLength: 0,
281+
text: "// single line comment\n",
282+
},
283+
{
284+
range: new vscode.Range(1, 0, 1, 0),
285+
rangeOffset: 1,
286+
rangeLength: 0,
287+
text: "/*\n * block comment\n */\n",
288+
},
289+
],
290+
} as vscode.TextDocumentChangeEvent;
291+
292+
let commentLinesTotal = 0;
293+
const originalIncrease = ProgressionController.increaseProgression;
294+
ProgressionController.increaseProgression = async (
295+
criteria: string,
296+
amount: number | string = 1,
297+
) => {
298+
if (criteria === constants.criteria.LINES_OF_COMMENTS) {
299+
commentLinesTotal += Number(amount);
300+
}
301+
};
302+
303+
try {
304+
await fileListeners.handleTextChangedEvent(event);
305+
assert.strictEqual(
306+
commentLinesTotal,
307+
4,
308+
"Should count: //, /*, * block comment, */",
309+
);
310+
} finally {
311+
ProgressionController.increaseProgression = originalIncrease;
312+
}
313+
});
314+
315+
test("handleTextChangedEvent should increase LINES_OF_COMMENTS when Enter is pressed after typing a comment", async () => {
316+
const testFile = path.join(tempDir, "test_typed_comment.ts");
317+
const uri = vscode.Uri.file(testFile);
318+
// Simulate pressing Enter at the end of a comment line already in the document
319+
const document = {
320+
fileName: testFile,
321+
uri: uri,
322+
lineAt: (_line: number) => ({ text: "// typed comment line" }),
323+
} as unknown as vscode.TextDocument;
324+
325+
const event = {
326+
document: document,
327+
reason: undefined,
328+
contentChanges: [
329+
{
330+
range: new vscode.Range(0, 21, 0, 21),
331+
rangeOffset: 21,
332+
rangeLength: 0,
333+
text: "\n",
334+
},
335+
],
336+
} as vscode.TextDocumentChangeEvent;
337+
338+
let commentLinesTotal = 0;
339+
const originalIncrease = ProgressionController.increaseProgression;
340+
ProgressionController.increaseProgression = async (
341+
criteria: string,
342+
amount: number | string = 1,
343+
) => {
344+
if (criteria === constants.criteria.LINES_OF_COMMENTS) {
345+
commentLinesTotal += Number(amount);
346+
}
347+
};
348+
349+
try {
350+
await fileListeners.handleTextChangedEvent(event);
351+
assert.strictEqual(
352+
commentLinesTotal,
353+
1,
354+
"Pressing Enter at end of a comment line should count 1 comment line",
355+
);
356+
} finally {
357+
ProgressionController.increaseProgression = originalIncrease;
358+
}
359+
});
360+
361+
test("handleTextChangedEvent should not count Enter after a non-comment line as LINES_OF_COMMENTS", async () => {
362+
const testFile = path.join(tempDir, "test_enter_code.ts");
363+
const uri = vscode.Uri.file(testFile);
364+
const document = {
365+
fileName: testFile,
366+
uri: uri,
367+
lineAt: (_line: number) => ({ text: "const x = 42;" }),
368+
} as unknown as vscode.TextDocument;
369+
370+
const event = {
371+
document: document,
372+
reason: undefined,
373+
contentChanges: [
374+
{
375+
range: new vscode.Range(0, 13, 0, 13),
376+
rangeOffset: 13,
377+
rangeLength: 0,
378+
text: "\n",
379+
},
380+
],
381+
} as vscode.TextDocumentChangeEvent;
382+
383+
let commentLinesCalled = false;
384+
const originalIncrease = ProgressionController.increaseProgression;
385+
ProgressionController.increaseProgression = async (criteria: string) => {
386+
if (criteria === constants.criteria.LINES_OF_COMMENTS) {
387+
commentLinesCalled = true;
388+
}
389+
};
390+
391+
try {
392+
await fileListeners.handleTextChangedEvent(event);
393+
assert.strictEqual(
394+
commentLinesCalled,
395+
false,
396+
"Pressing Enter after a code line should not count as a comment",
397+
);
398+
} finally {
399+
ProgressionController.increaseProgression = originalIncrease;
400+
}
401+
});
402+
403+
test("handleTextChangedEvent should not count non-comment lines as LINES_OF_COMMENTS", async () => {
404+
const testFile = path.join(tempDir, "test_no_comment.ts");
405+
const uri = vscode.Uri.file(testFile);
406+
const document = {
407+
fileName: testFile,
408+
uri: uri,
409+
} as vscode.TextDocument;
410+
411+
const event = {
412+
document: document,
413+
reason: undefined,
414+
contentChanges: [
415+
{
416+
range: new vscode.Range(0, 0, 0, 0),
417+
rangeOffset: 0,
418+
rangeLength: 0,
419+
text: "const x = 1;\nconst y = 2;\n",
420+
},
421+
],
422+
} as vscode.TextDocumentChangeEvent;
423+
424+
let commentLinesCalled = false;
425+
const originalIncrease = ProgressionController.increaseProgression;
426+
ProgressionController.increaseProgression = async (criteria: string) => {
427+
if (criteria === constants.criteria.LINES_OF_COMMENTS) {
428+
commentLinesCalled = true;
429+
}
430+
};
431+
432+
try {
433+
await fileListeners.handleTextChangedEvent(event);
434+
assert.strictEqual(
435+
commentLinesCalled,
436+
false,
437+
"Should not count code lines as comments",
438+
);
439+
} finally {
440+
ProgressionController.increaseProgression = originalIncrease;
441+
}
442+
});
443+
444+
test("handleTextChangedEvent should recognize various comment styles", async () => {
445+
const testFile = path.join(tempDir, "test_comment_styles.ts");
446+
const uri = vscode.Uri.file(testFile);
447+
const document = {
448+
fileName: testFile,
449+
uri: uri,
450+
} as vscode.TextDocument;
451+
452+
const commentStyles = [
453+
"// C-style single-line\n",
454+
"/* C-style block open\n",
455+
" * block continuation\n",
456+
" */\n",
457+
"# hash style\n",
458+
"-- SQL style\n",
459+
"% matlab style\n",
460+
"<!-- HTML comment\n",
461+
];
462+
463+
for (const commentLine of commentStyles) {
464+
const event = {
465+
document: document,
466+
reason: undefined,
467+
contentChanges: [
468+
{
469+
range: new vscode.Range(0, 0, 0, 0),
470+
rangeOffset: 0,
471+
rangeLength: 0,
472+
text: commentLine,
473+
},
474+
],
475+
} as vscode.TextDocumentChangeEvent;
476+
477+
let counted = false;
478+
const originalIncrease = ProgressionController.increaseProgression;
479+
ProgressionController.increaseProgression = async (criteria: string) => {
480+
if (criteria === constants.criteria.LINES_OF_COMMENTS) {
481+
counted = true;
482+
}
483+
};
484+
485+
try {
486+
await fileListeners.handleTextChangedEvent(event);
487+
assert.strictEqual(
488+
counted,
489+
true,
490+
`Should count as comment: ${JSON.stringify(commentLine)}`,
491+
);
492+
} finally {
493+
ProgressionController.increaseProgression = originalIncrease;
494+
}
495+
}
496+
});
497+
264498
test("handleDiagnosticChangedEvent should ignore configured URIs", async () => {
265499
const ignoredUri = vscode.Uri.file(
266500
path.join(tempDir, "node_modules", "broken.ts"),

0 commit comments

Comments
 (0)