vzcode
Version:
Multiplayer code editor system
202 lines (189 loc) • 5.6 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { randomId } from '../randomId.js';
import {
enableDirectories,
debugDirectories,
debugIgnore,
} from './featureFlags.js';
import createIgnore from 'ignore';
import { ignoreFilePattern, baseIgnore } from './config.js';
import { isDirectory } from './isDirectory.js';
/**
* @param {string} fullPath - absolut path of the workspace root
* @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
* @param {string} fileName - name of the ignore file
* @returns {string[]} parsed lines
*/
const parseIgnoreFile = (
fullPath,
currentDirectory,
fileName,
) => {
const filePath = path.join(
fullPath,
currentDirectory,
fileName,
);
const content = fs.readFileSync(filePath, 'utf8');
const globs = content
.split(/[\n\r]+/)
.filter(
// remove blank line and comments
(line) => line.length > 0 && !line.startsWith('#'),
)
.map((line) => {
const { bang, slash, glob } = line.match(
/^(?<bang>!?)(?<slash>\/?)(?<glob>.*)$/,
).groups;
const hasSlash =
Boolean(slash) || /\/.*\S/.test(glob);
const relativeGlob = path.posix.join(
currentDirectory.replace(
// escape characters with special meaning in glob expressions
/[*?!# \[\]\\]/g,
(char) => '\\' + char,
),
// a pattern that doesn't include a slash (not counting a trailing one) matches files in any descendant directory od the current one
hasSlash ? '' : '**',
glob,
);
// preserve leading `!` and `/` characters
return bang + slash + relativeGlob;
});
if (debugIgnore) {
console.debug(
'at',
currentDirectory,
'parsing',
fileName,
'obtained globs',
globs,
);
}
return globs;
};
// Lists files from the file system,
// converts them into the VZCode internal
// ShareDB-compatible data structure.
export const computeInitialDocument = ({ fullPath }) => {
// Isolate files, not directories.
// Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
// Initialize the document using our data structure for representing files.
const initialDocument = {
// `files`
// * Keys are file ids, which are random numbers.
// * Values are objects with properties:
// * text - the text content of the file
// * name - the file name
files: {},
// `isInteracting`
// * Whether the user is currently interacting with the document
// using an interactive code widget such as number dragger.
// * When true, the auto-save to the file system is changed to be
// more frequent (throttled not debounced).
// isInteracting: false,
// This is `undefined` initially.
};
/**
* Stack for recursively traversing directories.
* @type {string[]}
*/
let files = [];
const ignoreFileMatcher = createIgnore().add(
ignoreFilePattern,
);
const isIgnoreFile = (fileName) =>
ignoreFileMatcher.ignores(fileName);
const unsearchedDirectories = [
{
currentDirectory: '.',
ignore: createIgnore().add(baseIgnore),
},
];
while (unsearchedDirectories.length !== 0) {
const { currentDirectory, ignore: parentIgnore } =
unsearchedDirectories.pop();
const currentDirectoryPath = path.join(
fullPath,
currentDirectory,
);
const dirEntries = fs
.readdirSync(currentDirectoryPath, {
withFileTypes: true,
})
.filter((dirent) =>
enableDirectories ? true : dirent.isFile(),
);
// find .ignore or .gitignore files in the current directory
const ignoreFiles = dirEntries
.filter(
(dirent) =>
dirent.isFile() && isIgnoreFile(dirent.name),
)
.map((file) => file.name);
let ignore = parentIgnore;
if (ignoreFiles.length > 0) {
const globs = ignoreFiles.flatMap((fileName) =>
parseIgnoreFile(
fullPath,
currentDirectory,
fileName,
),
);
ignore = createIgnore().add(parentIgnore).add(globs);
}
const newFiles = dirEntries
.filter((dirent) => {
const relativePath =
path.posix.join(currentDirectory, dirent.name) +
(dirent.isDirectory() ? '/' : '');
const keep = !ignore.ignores(relativePath);
if (debugIgnore && !keep) {
console.debug(
'at',
currentDirectory,
'ignoring',
relativePath,
);
}
return keep;
})
// Add a trailing slash for directories
.map((dirent) => {
const relativePath = path.posix.join(
currentDirectory,
dirent.name,
);
if (!dirent.isDirectory()) {
return relativePath;
}
unsearchedDirectories.push({
currentDirectory: relativePath,
ignore,
});
return relativePath + '/';
});
// console.log(currentDirectory);
files.push(...newFiles);
}
files.forEach((file) => {
const id = randomId();
initialDocument.files[id] = {
text: isDirectory(file)
? null
: fs.readFileSync(
path.join(fullPath, file),
'utf-8',
),
name: file,
};
});
if (debugDirectories) {
console.log('files:');
console.log(files);
console.log('initialDocument:');
console.log(JSON.stringify(initialDocument, null, 2));
}
return initialDocument;
};