mirror of
https://github.com/foambubble/foam.git
synced 2026-01-09 14:08:13 -05:00
Implement `foam.files.include` configuration to allow users to specify which files/directories Foam should include. Also migrating `foam.files.ignore` to `foam.files.exclude` Fixes #1422
This commit is contained in:
@@ -549,7 +549,24 @@
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
],
|
||||
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`"
|
||||
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore all the content of a given folder, use `<folderName>/**/*`",
|
||||
"deprecationMessage": "Use 'foam.files.exclude' instead. This setting will be removed in a future version."
|
||||
},
|
||||
"foam.files.exclude": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"default": [],
|
||||
"description": "Specifies the list of globs that will be excluded by Foam (e.g. they will not be considered when creating the graph). To exclude all the content of a given folder, use `<folderName>/**/*`. This setting is combined with 'foam.files.ignore' (deprecated) and 'files.exclude'."
|
||||
},
|
||||
"foam.files.include": {
|
||||
"type": [
|
||||
"array"
|
||||
],
|
||||
"default": [
|
||||
"**/*"
|
||||
],
|
||||
"description": "Specifies the list of glob patterns for files to include in Foam. Files must match at least one include pattern and not match any exclude patterns. Use this to limit Foam to specific directories (e.g., [\"notes/**\"]) or file types (e.g., [\"**/*.md\"]). Defaults to all files."
|
||||
},
|
||||
"foam.files.attachmentExtensions": {
|
||||
"type": "string",
|
||||
|
||||
@@ -94,8 +94,15 @@ export class FileListBasedMatcher implements IMatcher {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
|
||||
constructor(files: URI[], private readonly listFiles: () => Promise<URI[]>) {
|
||||
constructor(
|
||||
files: URI[],
|
||||
private readonly listFiles: () => Promise<URI[]>,
|
||||
include: string[] = ['**/*'],
|
||||
exclude: string[] = []
|
||||
) {
|
||||
this.files = files.map(f => f.path);
|
||||
this.include = include;
|
||||
this.exclude = exclude;
|
||||
}
|
||||
|
||||
match(files: URI[]): URI[] {
|
||||
@@ -110,9 +117,13 @@ export class FileListBasedMatcher implements IMatcher {
|
||||
this.files = (await this.listFiles()).map(f => f.path);
|
||||
}
|
||||
|
||||
static async createFromListFn(listFiles: () => Promise<URI[]>) {
|
||||
static async createFromListFn(
|
||||
listFiles: () => Promise<URI[]>,
|
||||
include: string[] = ['**/*'],
|
||||
exclude: string[] = []
|
||||
) {
|
||||
const files = await listFiles();
|
||||
return new FileListBasedMatcher(files, listFiles);
|
||||
return new FileListBasedMatcher(files, listFiles, include, exclude);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import {
|
||||
getAttachmentsExtensions,
|
||||
getIgnoredFilesSetting,
|
||||
getExcludedFilesSetting,
|
||||
getIncludeFilesSetting,
|
||||
getNotesExtensions,
|
||||
} from './settings';
|
||||
import { AttachmentResourceProvider } from './core/services/attachment-provider';
|
||||
@@ -33,14 +34,15 @@ export async function activate(context: ExtensionContext) {
|
||||
}
|
||||
|
||||
// Prepare Foam
|
||||
const excludes = getIgnoredFilesSetting().map(g => g.toString());
|
||||
const { matcher, dataStore, excludePatterns } =
|
||||
await createMatcherAndDataStore(excludes);
|
||||
const includes = getIncludeFilesSetting().map(g => g.toString());
|
||||
const excludes = getExcludedFilesSetting().map(g => g.toString());
|
||||
const { matcher, dataStore, includePatterns, excludePatterns } =
|
||||
await createMatcherAndDataStore(includes, excludes);
|
||||
|
||||
Logger.info('Loading from directories:');
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
Logger.info('- ' + folder.uri.fsPath);
|
||||
Logger.info(' Include: **/*');
|
||||
Logger.info(' Include: ' + includePatterns.get(folder.name).join(','));
|
||||
Logger.info(' Exclude: ' + excludePatterns.get(folder.name).join(','));
|
||||
}
|
||||
|
||||
@@ -98,6 +100,8 @@ export async function activate(context: ExtensionContext) {
|
||||
if (
|
||||
[
|
||||
'foam.files.ignore',
|
||||
'foam.files.exclude',
|
||||
'foam.files.include',
|
||||
'foam.files.attachmentExtensions',
|
||||
'foam.files.noteExtensions',
|
||||
'foam.files.defaultNoteExtension',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getAttachmentsExtensions } from '../../settings';
|
||||
import {
|
||||
getAttachmentsExtensions,
|
||||
getIncludeFilesSetting,
|
||||
} from '../../settings';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
@@ -21,6 +24,7 @@ export default async function activate(
|
||||
const foam = await foamPromise;
|
||||
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getIncludeFilesSetting().map(g => g.toString()),
|
||||
getOrphansConfig().exclude
|
||||
);
|
||||
const provider = new OrphanTreeView(
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FoamGraph } from '../../core/model/graph';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FolderTreeItem } from './utils/folder-tree-provider';
|
||||
import { getIncludeFilesSetting } from '../../settings';
|
||||
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
@@ -31,6 +32,7 @@ export default async function activate(
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
const { matcher } = await createMatcherAndDataStore(
|
||||
getIncludeFilesSetting().map(g => g.toString()),
|
||||
getPlaceholdersConfig().exclude
|
||||
);
|
||||
const provider = new PlaceholderTreeView(
|
||||
|
||||
@@ -225,14 +225,38 @@ export function asAbsoluteWorkspaceUri(
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
export async function createMatcherAndDataStore(
|
||||
includes: string[],
|
||||
excludes: string[]
|
||||
): Promise<{
|
||||
matcher: IMatcher;
|
||||
dataStore: IDataStore;
|
||||
includePatterns: Map<string, string[]>;
|
||||
excludePatterns: Map<string, string[]>;
|
||||
}> {
|
||||
const includePatterns = new Map<string, string[]>();
|
||||
const excludePatterns = new Map<string, string[]>();
|
||||
workspace.workspaceFolders.forEach(f => excludePatterns.set(f.name, []));
|
||||
workspace.workspaceFolders.forEach(f => {
|
||||
includePatterns.set(f.name, []);
|
||||
excludePatterns.set(f.name, []);
|
||||
});
|
||||
|
||||
// Process include patterns
|
||||
for (const include of includes) {
|
||||
const tokens = include.split('/');
|
||||
const matchesFolder = workspace.workspaceFolders.find(
|
||||
f => f.name === tokens[0]
|
||||
);
|
||||
if (matchesFolder) {
|
||||
includePatterns.get(tokens[0]).push(tokens.slice(1).join('/'));
|
||||
} else {
|
||||
for (const [, value] of includePatterns.entries()) {
|
||||
value.push(include);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process exclude patterns
|
||||
for (const exclude of excludes) {
|
||||
const tokens = exclude.split('/');
|
||||
const matchesFolder = workspace.workspaceFolders.find(
|
||||
@@ -248,19 +272,41 @@ export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
}
|
||||
|
||||
const listFiles = async () => {
|
||||
let files: Uri[] = [];
|
||||
let allFiles: Uri[] = [];
|
||||
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
const uris = await workspace.findFiles(
|
||||
new RelativePattern(folder.uri, '**/*'),
|
||||
new RelativePattern(
|
||||
folder.uri,
|
||||
`{${excludePatterns.get(folder.name).join(',')}}`
|
||||
)
|
||||
const folderIncludes = includePatterns.get(folder.name);
|
||||
const folderExcludes = excludePatterns.get(folder.name);
|
||||
const excludePattern =
|
||||
folderExcludes.length > 0
|
||||
? new RelativePattern(folder.uri, `{${folderExcludes.join(',')}}`)
|
||||
: null;
|
||||
|
||||
// If includes are empty, include nothing
|
||||
if (folderIncludes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filesFromAllPatterns: Uri[] = [];
|
||||
|
||||
// Apply each include pattern
|
||||
for (const includePattern of folderIncludes) {
|
||||
const uris = await workspace.findFiles(
|
||||
new RelativePattern(folder.uri, includePattern),
|
||||
excludePattern
|
||||
);
|
||||
filesFromAllPatterns.push(...uris);
|
||||
}
|
||||
|
||||
// Deduplicate files (same file may match multiple patterns)
|
||||
const uniqueFiles = Array.from(
|
||||
new Map(filesFromAllPatterns.map(uri => [uri.fsPath, uri])).values()
|
||||
);
|
||||
files = [...files, ...uris];
|
||||
|
||||
allFiles = [...allFiles, ...uniqueFiles];
|
||||
}
|
||||
|
||||
return files.map(fromVsCodeUri);
|
||||
return allFiles.map(fromVsCodeUri);
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
@@ -270,9 +316,14 @@ export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
};
|
||||
|
||||
const dataStore = new GenericDataStore(listFiles, readFile);
|
||||
const matcher = isEmpty(excludes)
|
||||
? new AlwaysIncludeMatcher()
|
||||
: await FileListBasedMatcher.createFromListFn(listFiles);
|
||||
const matcher =
|
||||
isEmpty(excludes) && includes.length === 1 && includes[0] === '**/*'
|
||||
? new AlwaysIncludeMatcher()
|
||||
: await FileListBasedMatcher.createFromListFn(
|
||||
listFiles,
|
||||
includes,
|
||||
excludes
|
||||
);
|
||||
|
||||
return { matcher, dataStore, excludePatterns };
|
||||
return { matcher, dataStore, includePatterns, excludePatterns };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* @unit-ready */
|
||||
import { getNotesExtensions } from './settings';
|
||||
import { getNotesExtensions, getIncludeFilesSetting } from './settings';
|
||||
import { withModifiedFoamConfiguration } from './test/test-utils-vscode';
|
||||
|
||||
describe('Default note settings', () => {
|
||||
@@ -31,3 +31,53 @@ describe('Default note settings', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Include files settings', () => {
|
||||
it('should default to **/* when not configured', () => {
|
||||
const includes = getIncludeFilesSetting();
|
||||
expect(includes).toEqual(['**/*']);
|
||||
});
|
||||
|
||||
it('should return custom include patterns when configured', async () => {
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.include',
|
||||
['notes/**'],
|
||||
async () => {
|
||||
const includes = getIncludeFilesSetting();
|
||||
expect(includes).toEqual(['notes/**']);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should support multiple include patterns', async () => {
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.include',
|
||||
['docs/**', 'notes/**', '**/*.md'],
|
||||
async () => {
|
||||
const includes = getIncludeFilesSetting();
|
||||
expect(includes).toEqual(['docs/**', 'notes/**', '**/*.md']);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should expand alternate groups in include patterns', async () => {
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.include',
|
||||
['**/*.{md,mdx,markdown}'],
|
||||
async () => {
|
||||
const includes = getIncludeFilesSetting();
|
||||
expect(includes).toEqual(
|
||||
expect.arrayContaining(['**/*.md', '**/*.mdx', '**/*.markdown'])
|
||||
);
|
||||
expect(includes.length).toBe(3);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty array when configured with empty array', async () => {
|
||||
await withModifiedFoamConfiguration('files.include', [], async () => {
|
||||
const includes = getIncludeFilesSetting();
|
||||
expect(includes).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,11 +39,20 @@ export function getAttachmentsExtensions() {
|
||||
.map(ext => '.' + ext.trim());
|
||||
}
|
||||
|
||||
/** Retrieve the list of file ignoring globs. */
|
||||
export function getIgnoredFilesSetting(): GlobPattern[] {
|
||||
/** Retrieve the list of file exclude globs. */
|
||||
export function getExcludedFilesSetting(): GlobPattern[] {
|
||||
return [
|
||||
'**/.foam/**',
|
||||
...workspace.getConfiguration().get('foam.files.ignore', []),
|
||||
...workspace.getConfiguration().get('foam.files.exclude', []),
|
||||
...workspace.getConfiguration().get('foam.files.ignore', []), // deprecated, for backward compatibility
|
||||
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
|
||||
].flatMap(expandAlternateGroups);
|
||||
}
|
||||
|
||||
/** Retrieve the list of file include globs. */
|
||||
export function getIncludeFilesSetting(): GlobPattern[] {
|
||||
return workspace
|
||||
.getConfiguration()
|
||||
.get('foam.files.include', ['**/*'])
|
||||
.flatMap(expandAlternateGroups);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user