Skip to content

Commit 0f71438

Browse files
committed
initial commit
0 parents  commit 0f71438

12 files changed

Lines changed: 411 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.vscode
2+
node_modules/
3+
out/
4+
pnpm-lock.yaml

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enable-pre-post-scripts = true

.vscode-test.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { defineConfig } from '@vscode/test-cli';
2+
3+
export default defineConfig({
4+
files: 'out/test/**/*.test.js',
5+
});

package.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "codecount",
3+
"displayName": "CodeCount",
4+
"description": "A tool for learn how much line of code your project contains",
5+
"version": "0.0.1",
6+
"engines": {
7+
"vscode": "^1.108.2"
8+
},
9+
"categories": [
10+
"Other"
11+
],
12+
"activationEvents": [
13+
"onCommand:codecount.countLines",
14+
"onCommand:codecount.countFilesLines",
15+
"onCommand:codecount.countLinesByExtension"
16+
],
17+
"main": "./out/extension.js",
18+
"contributes": {
19+
"commands": [
20+
{
21+
"command": "codecount.helloWorld",
22+
"title": "Hello World"
23+
},
24+
{
25+
"command": "codecount.countLines",
26+
"title": "Count Lines of Code"
27+
},
28+
{
29+
"command": "codecount.countFilesLines",
30+
"title": "Count Lines of Code in All Files"
31+
},
32+
{
33+
"command": "codecount.countLinesByExtension",
34+
"title": "Count Lines of Code by Extension"
35+
}
36+
]
37+
},
38+
"scripts": {
39+
"vscode:prepublish": "pnpm run compile",
40+
"compile": "tsc -p ./",
41+
"watch": "tsc -watch -p ./",
42+
"pretest": "pnpm run compile && pnpm run lint",
43+
"lint": "eslint src",
44+
"test": "vscode-test"
45+
},
46+
"devDependencies": {
47+
"@types/vscode": "^1.109.0",
48+
"@types/mocha": "^10.0.10",
49+
"@types/node": "22.x",
50+
"typescript-eslint": "^8.54.0",
51+
"eslint": "^9.39.2",
52+
"typescript": "^5.9.3",
53+
"@vscode/test-cli": "^0.0.12",
54+
"@vscode/test-electron": "^2.5.2"
55+
}
56+
}

src/extension.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as vscode from 'vscode';
2+
import { LineCounter } from './services/LineCounter';
3+
import { GitignoreService } from './services/GitignoreService';
4+
import { WorkspaceLineCounter } from './services/WorkspaceLineCounter';
5+
import { LineCounterByFileFormat } from './services/LineCounterByFileFormat';
6+
7+
export function activate(context: vscode.ExtensionContext) {
8+
const lineCounter = new LineCounter();
9+
const gitignoreService = new GitignoreService();
10+
const workspaceLineCounter = new WorkspaceLineCounter(lineCounter, gitignoreService);
11+
const lineCounterByFileFormat = new LineCounterByFileFormat(workspaceLineCounter);
12+
13+
const fileline = vscode.commands.registerCommand('codecount.countLines', () => {
14+
const editor = vscode.window.activeTextEditor;
15+
if (!editor) {
16+
vscode.window.showWarningMessage('No active editor found.');
17+
return;
18+
}
19+
20+
const lineCount = lineCounter.countDocumentLines(editor.document);
21+
const fileName = editor.document.fileName.split(/[/\\]/).pop() ?? 'Untitled';
22+
vscode.window.showInformationMessage(`Total lines in ${fileName}: ${lineCount}`);
23+
});
24+
25+
const fileslines = vscode.commands.registerCommand('codecount.countFilesLines', async () => {
26+
if (!vscode.workspace.workspaceFolders?.length) {
27+
vscode.window.showWarningMessage('No workspace folder found.');
28+
return;
29+
}
30+
31+
const files = await vscode.workspace.findFiles(
32+
'**/*',
33+
'**/{node_modules,out,dist,.git}/**'
34+
);
35+
if (files.length === 0) {
36+
vscode.window.showInformationMessage('No files found in workspace.');
37+
return;
38+
}
39+
const result = await workspaceLineCounter.countWorkspaceLines(files);
40+
if (result.fileCount === 0) {
41+
vscode.window.showInformationMessage('All files are ignored by .gitignore.');
42+
return;
43+
}
44+
vscode.window.showInformationMessage(
45+
`Total lines in workspace (${result.fileCount} files): ${result.totalLines}. Comment lines: ${result.commentLines}`
46+
);
47+
});
48+
49+
const fileslinesByExtension = vscode.commands.registerCommand('codecount.countLinesByExtension', async () => {
50+
if (!vscode.workspace.workspaceFolders?.length) {
51+
vscode.window.showWarningMessage('No workspace folder found.');
52+
return;
53+
}
54+
55+
const result = await lineCounterByFileFormat.countByExtension();
56+
if (!result) {
57+
return;
58+
}
59+
if (result.fileCount === 0) {
60+
vscode.window.showInformationMessage('All files are ignored by .gitignore.');
61+
return;
62+
}
63+
64+
vscode.window.showInformationMessage(
65+
`Total lines in matched files (${result.fileCount} files): ${result.totalLines}. Comment lines: ${result.commentLines}`
66+
);
67+
});
68+
69+
context.subscriptions.push(fileline);
70+
context.subscriptions.push(fileslines);
71+
context.subscriptions.push(fileslinesByExtension);
72+
}
73+
74+
// This method is called when your extension is deactivated
75+
export function deactivate() {}

src/services/GitignoreService.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import type { GitignoreMatcher, GitignoreRule } from '../types/gitignore';
4+
5+
export class GitignoreService {
6+
private readonly cache = new Map<string, GitignoreMatcher>();
7+
8+
async getMatcher(rootPath: string): Promise<GitignoreMatcher> {
9+
const cached = this.cache.get(rootPath);
10+
if (cached) {
11+
return cached;
12+
}
13+
14+
const matcher = await this.loadGitignore(rootPath);
15+
this.cache.set(rootPath, matcher);
16+
return matcher;
17+
}
18+
19+
private async loadGitignore(rootPath: string): Promise<GitignoreMatcher> {
20+
const gitignoreUri = vscode.Uri.file(path.join(rootPath, '.gitignore'));
21+
let content = '';
22+
try {
23+
const data = await vscode.workspace.fs.readFile(gitignoreUri);
24+
content = data.toString();
25+
} catch {
26+
// .gitignore not found or not readable; ignore silently
27+
}
28+
29+
const rules = this.parseGitignore(content);
30+
return {
31+
ignores: (relativePath: string) => this.matchGitignore(rules, relativePath)
32+
};
33+
}
34+
35+
private parseGitignore(content: string): GitignoreRule[] {
36+
const rules: GitignoreRule[] = [];
37+
const lines = content.split(/\r?\n/);
38+
for (const rawLine of lines) {
39+
if (!rawLine || rawLine.trim().length === 0) {
40+
continue;
41+
}
42+
43+
let line = rawLine;
44+
let negate = false;
45+
if (line.startsWith('\\#')) {
46+
line = line.slice(1);
47+
} else if (line.startsWith('#')) {
48+
continue;
49+
}
50+
51+
if (line.startsWith('!')) {
52+
negate = true;
53+
line = line.slice(1);
54+
}
55+
56+
if (line.trim().length === 0) {
57+
continue;
58+
}
59+
60+
const isDirectory = line.endsWith('/');
61+
const pattern = isDirectory ? line.slice(0, -1) : line;
62+
const regex = this.gitignorePatternToRegex(pattern);
63+
rules.push({ negate, regex, isDirectory });
64+
}
65+
66+
return rules;
67+
}
68+
69+
private matchGitignore(rules: GitignoreRule[], relativePath: string): boolean {
70+
const normalized = relativePath.replace(/\\/g, '/');
71+
let ignored = false;
72+
for (const rule of rules) {
73+
const matches = rule.isDirectory
74+
? rule.regex.test(normalized + '/')
75+
: rule.regex.test(normalized);
76+
if (matches) {
77+
ignored = !rule.negate;
78+
}
79+
}
80+
81+
return ignored;
82+
}
83+
84+
private gitignorePatternToRegex(pattern: string): RegExp {
85+
let pat = pattern.replace(/\\/g, '/');
86+
let anchored = false;
87+
if (pat.startsWith('/')) {
88+
anchored = true;
89+
pat = pat.slice(1);
90+
}
91+
92+
const escaped = pat
93+
.split(/(\*\*|\*|\?)/g)
94+
.map((part) => {
95+
if (part === '**') {
96+
return '.*';
97+
}
98+
if (part === '*') {
99+
return '[^/]*';
100+
}
101+
if (part === '?') {
102+
return '[^/]';
103+
}
104+
return part.replace(/[.+^${}()|[\]\\]/g, '\\$&');
105+
})
106+
.join('');
107+
108+
const prefix = anchored ? '^' : '(^|.*/)';
109+
return new RegExp(`${prefix}${escaped}(/|$)`);
110+
}
111+
}

src/services/LineCounter.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as vscode from 'vscode';
2+
import type { LineCountResult } from '../types/lineCount';
3+
4+
export class LineCounter {
5+
countDocumentLines(document: { lineCount: number }): number {
6+
let totalLines = 0;
7+
for (let i = 0; i < document.lineCount; i += 1) {
8+
const lineText = (document as vscode.TextDocument).lineAt(i).text;
9+
if (this.isCodeLine(lineText)) {
10+
totalLines += 1;
11+
}
12+
}
13+
14+
return totalLines;
15+
}
16+
17+
async countWorkspaceLines(
18+
uris: readonly vscode.Uri[],
19+
getDocument: (uri: vscode.Uri) => Thenable<vscode.TextDocument>
20+
): Promise<LineCountResult> {
21+
let totalLines = 0;
22+
let commentLines = 0;
23+
for (const uri of uris) {
24+
const document = await getDocument(uri);
25+
for (let i = 0; i < document.lineCount; i += 1) {
26+
const lineText = document.lineAt(i).text;
27+
if (this.isCodeLine(lineText)) {
28+
totalLines += 1;
29+
}
30+
else if (this.isCommentLine(lineText)) {
31+
commentLines += 1;
32+
}
33+
}
34+
}
35+
36+
return { totalLines, fileCount: uris.length, commentLines };
37+
}
38+
39+
private isCommentLine(lineText: string): boolean {
40+
const trimmed = lineText.trim();
41+
return trimmed.startsWith('//') || trimmed.startsWith('#');
42+
}
43+
44+
private isCodeLine(lineText: string): boolean {
45+
const trimmed = lineText.trim();
46+
return trimmed.length > 0 && !this.isCommentLine(trimmed);
47+
}
48+
49+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as vscode from 'vscode';
2+
import type { LineCountResult } from '../types/lineCount';
3+
import { WorkspaceLineCounter } from './WorkspaceLineCounter';
4+
5+
export class LineCounterByFileFormat {
6+
7+
constructor(private readonly workspaceLineCounter: WorkspaceLineCounter) {}
8+
9+
async countByExtension(): Promise<LineCountResult | undefined> {
10+
const extensionInput = await vscode.window.showInputBox({
11+
prompt: 'Enter file extension (e.g., .py or py)',
12+
placeHolder: '.py'
13+
});
14+
if (!extensionInput) {
15+
return;
16+
}
17+
18+
const normalized = extensionInput.startsWith('.')
19+
? extensionInput
20+
: '.' + extensionInput;
21+
const extension = normalized.trim();
22+
if (extension === '.') {
23+
vscode.window.showWarningMessage('Invalid file extension.');
24+
return;
25+
}
26+
27+
const files = await vscode.workspace.findFiles(
28+
'**/*' + extension,
29+
'**/{node_modules,out,dist,.git}/**'
30+
);
31+
if (files.length === 0) {
32+
vscode.window.showInformationMessage('No files found for ' + extension + '.');
33+
return;
34+
}
35+
36+
return this.workspaceLineCounter.countWorkspaceLines(files);
37+
}
38+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import { LineCounter } from './LineCounter';
4+
import { GitignoreService } from './GitignoreService';
5+
import type { LineCountResult } from '../types/lineCount';
6+
7+
export class WorkspaceLineCounter {
8+
constructor(
9+
private readonly lineCounter: LineCounter,
10+
private readonly gitignoreService: GitignoreService
11+
) {}
12+
13+
async countWorkspaceLines(files: readonly vscode.Uri[]): Promise<LineCountResult> {
14+
const filteredFiles = await this.filterIgnoredFiles(files);
15+
if (filteredFiles.length === 0) {
16+
return { totalLines: 0, fileCount: 0, commentLines: 0 };
17+
}
18+
19+
return this.lineCounter.countWorkspaceLines(filteredFiles, (uri) => vscode.workspace.openTextDocument(uri));
20+
}
21+
22+
private async filterIgnoredFiles(files: readonly vscode.Uri[]): Promise<vscode.Uri[]> {
23+
const filteredFiles: vscode.Uri[] = [];
24+
for (const uri of files) {
25+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
26+
if (!workspaceFolder) {
27+
filteredFiles.push(uri);
28+
continue;
29+
}
30+
31+
const rootPath = workspaceFolder.uri.fsPath;
32+
const matcher = await this.gitignoreService.getMatcher(rootPath);
33+
const relativePath = path.relative(rootPath, uri.fsPath).replace(/\\/g, '/');
34+
if (!matcher.ignores(relativePath)) {
35+
filteredFiles.push(uri);
36+
}
37+
}
38+
39+
return filteredFiles;
40+
}
41+
}

0 commit comments

Comments
 (0)