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
36 changes: 31 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { JetTreeMarkViewProvider } from './JetTreeMarkViewProvider';
import { collectDirectoryGitignorePatterns, shouldExcludeByGitignore, GitignorePattern } from './gitignore-utils';

// noinspection JSUnusedGlobalSymbols
export function activate(ctx: vscode.ExtensionContext) {
Expand Down Expand Up @@ -43,9 +44,20 @@ interface TreeNodeType {

/**
* Build a TreeNodeType *for* the directory itself, including its contents.
* @param dir Directory path
* @param parentPatterns Optional gitignore patterns from parent directories
* @param forceUncheck
* @returns TreeNodeType representing the directory
*/
export function buildTreeNode(dir: string): TreeNodeType {
export function buildTreeNode(dir: string, parentPatterns: GitignorePattern[] = [], forceUncheck: boolean = false): TreeNodeType {
const name = path.basename(dir) || dir;

// Collect gitignore patterns for this directory
const directoryPatterns = collectDirectoryGitignorePatterns(dir);

// Combine with parent patterns (parent patterns take precedence)
const allPatterns = [...directoryPatterns, ...parentPatterns];

const node: TreeNodeType = {
id: dir,
name,
Expand All @@ -61,17 +73,31 @@ export function buildTreeNode(dir: string): TreeNodeType {

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
node.children!.push(buildTreeNode(fullPath));
const isDirectory = entry.isDirectory();

// Check if this file/folder should be excluded based on gitignore patterns
const shouldExclude = shouldExcludeByGitignore(fullPath, dir, allPatterns, isDirectory);

if (isDirectory) {
// Process subdirectory, passing down the combined patterns
const childNode = buildTreeNode(fullPath, allPatterns, shouldExclude || forceUncheck);

// If the directory itself matches gitignore patterns, mark it as unchecked
if (shouldExclude || forceUncheck) {
childNode.checked = false;
}

node.children!.push(childNode);
} else {
// Add file node
node.children!.push({
id: fullPath,
name: entry.name,
type: 'file',
checked: true
checked: !shouldExclude && !forceUncheck // Set checked to false if it matches gitignore patterns
});
}
}

return node;
}
}
199 changes: 199 additions & 0 deletions src/gitignore-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import * as fs from 'fs';
import * as path from 'path';

/**
* Represents a parsed gitignore pattern
*/
export interface GitignorePattern {
pattern: string;
isNegated: boolean;
isDirectory: boolean;
isAbsolute: boolean;
}

/**
* Parses a .gitignore file and returns an array of patterns
* @param gitignorePath Path to the .gitignore file
* @returns Array of parsed gitignore patterns
*/
export function parseGitignoreFile(gitignorePath: string): GitignorePattern[] {
if (!fs.existsSync(gitignorePath)) {
return [];
}

const content = fs.readFileSync(gitignorePath, 'utf8');
return parseGitignoreContent(content);
}

/**
* Parses gitignore content and returns an array of patterns
* @param content Content of the .gitignore file
* @returns Array of parsed gitignore patterns
*/
export function parseGitignoreContent(content: string): GitignorePattern[] {
return content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#')) // Remove empty lines and comments
.map(line => {
const isNegated = line.startsWith('!');
const pattern = isNegated ? line.substring(1) : line;
const isDirectory = pattern.endsWith('/');
const isAbsolute = pattern.startsWith('/') || pattern.startsWith('./');

return {
pattern: isAbsolute ? pattern.substring(pattern.startsWith('./') ? 2 : 1) : pattern,
isNegated,
isDirectory,
isAbsolute
};
});
}

/**
* Checks if a file or directory matches any of the gitignore patterns
* @param filePath Path to the file or directory (relative to the directory containing the .gitignore)
* @param patterns Array of gitignore patterns
* @param isDirectory Whether the path is a directory
* @returns True if the file or directory should be ignored
*/
export function matchesGitignorePatterns(
filePath: string,
patterns: GitignorePattern[],
isDirectory: boolean
): boolean {
// Normalize path for matching
const normalizedPath = filePath.replace(/\\/g, '/');

// Start with not ignored, then apply patterns in order
let ignored = false;

for (const pattern of patterns) {
// Skip directory-only patterns if this is a file
if (pattern.isDirectory && !isDirectory) {
continue;
}

if (matchesPattern(normalizedPath, pattern, isDirectory)) {
// If pattern matches, set ignored based on whether it's negated
ignored = !pattern.isNegated;
}
}

return ignored;
}

/**
* Checks if a path matches a gitignore pattern
* @param normalizedPath Normalized path to check
* @param pattern Gitignore pattern
* @param isDirectory Whether the path is a directory
* @returns True if the path matches the pattern
*/
function matchesPattern(
normalizedPath: string,
pattern: GitignorePattern,
isDirectory: boolean
): boolean {
const patternStr = pattern.pattern.replace(/\\/g, '/');

// For directory patterns (ending with /), also check without the trailing slash
// when matching against directories
if (pattern.isDirectory && isDirectory && patternStr.endsWith('/')) {
const patternWithoutSlash = patternStr.slice(0, -1);
if (normalizedPath === patternWithoutSlash) {
return true;
}
}

// Handle exact matches
if (!patternStr.includes('*')) {
if (pattern.isAbsolute) {
// For absolute patterns, match from the beginning
return normalizedPath === patternStr ||
(isDirectory && normalizedPath.startsWith(patternStr + '/'));
} else {
// For relative patterns, match anywhere in the path
return normalizedPath === patternStr ||
normalizedPath.endsWith('/' + patternStr) ||
normalizedPath.includes('/' + patternStr + '/') ||
(isDirectory && (
normalizedPath.endsWith('/' + patternStr) ||
normalizedPath.includes('/' + patternStr + '/')
));
}
}

// Handle wildcard patterns
const regexPattern = patternStr
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\?/g, '.'); // Convert ? to .

const regex = pattern.isAbsolute
? new RegExp(`^${regexPattern}$`)
: new RegExp(`(^|/)${regexPattern}$`);

return regex.test(normalizedPath);
}

/**
* Collects gitignore patterns from a specific directory
* @param dirPath Path to the directory
* @returns Array of gitignore patterns from this directory
*/
export function collectDirectoryGitignorePatterns(dirPath: string): GitignorePattern[] {
const gitignorePath = path.join(dirPath, '.gitignore');
if (fs.existsSync(gitignorePath)) {
return parseGitignoreFile(gitignorePath);
}
return [];
}

/**
* Collects all gitignore patterns that apply to a given directory
* @param dirPath Path to the directory
* @returns Array of gitignore patterns that apply to the directory
*/
export function collectGitignorePatterns(dirPath: string): GitignorePattern[] {
const patterns: GitignorePattern[] = [];
let currentDir = dirPath;

// Collect patterns from all parent directories up to the root
while (true) {
const gitignorePath = path.join(currentDir, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const dirPatterns = parseGitignoreFile(gitignorePath);
patterns.push(...dirPatterns);
}

const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Reached the root
}
currentDir = parentDir;
}

return patterns;
}

/**
* Determines if a file or directory should be excluded based on gitignore patterns
* @param fullPath Full path to the file or directory
* @param baseDir Base directory for relative path calculation
* @param patterns Gitignore patterns to check against
* @param isDirectory Whether the path is a directory
* @returns True if the file or directory should be excluded
*/
export function shouldExcludeByGitignore(
fullPath: string,
baseDir: string,
patterns: GitignorePattern[],
isDirectory: boolean
): boolean {
// Calculate path relative to the base directory
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');

// Check if the path matches any gitignore pattern
return matchesGitignorePatterns(relativePath, patterns, isDirectory);
}
Loading