mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-21 02:24:43 +02:00
1154 lines
36 KiB
TypeScript
1154 lines
36 KiB
TypeScript
import { parseWinPath } from './parseWinPath';
|
|
import { Telemetry } from './Telemetry';
|
|
import { Notifications } from './Notifications';
|
|
import {
|
|
commands,
|
|
Uri,
|
|
workspace,
|
|
window,
|
|
WorkspaceConfiguration,
|
|
FileSystemWatcher,
|
|
Disposable,
|
|
ProgressLocation
|
|
} from 'vscode';
|
|
import { ContentType, CustomTaxonomy, Project } from '../models';
|
|
import {
|
|
EXTENSION_NAME,
|
|
CONFIG_KEY,
|
|
CONTEXT,
|
|
ExtensionState,
|
|
SETTING_TAXONOMY_CUSTOM,
|
|
TelemetryEvent,
|
|
COMMAND_NAME,
|
|
SETTING_TAXONOMY_CONTENT_TYPES,
|
|
SETTING_CONTENT_PAGE_FOLDERS,
|
|
SETTING_CONTENT_SNIPPETS,
|
|
SETTING_CONTENT_PLACEHOLDERS,
|
|
SETTING_CUSTOM_SCRIPTS,
|
|
SETTING_DATA_FILES,
|
|
SETTING_DATA_TYPES,
|
|
SETTING_DATA_FOLDERS,
|
|
SETTING_EXTENDS,
|
|
SETTING_CONTENT_SORTING,
|
|
SETTING_GLOBAL_MODES,
|
|
SETTING_TAXONOMY_FIELD_GROUPS,
|
|
SETTING_CONTENT_DRAFT_FIELD,
|
|
SETTING_CONTENT_SUPPORTED_FILETYPES,
|
|
SETTING_GLOBAL_NOTIFICATIONS,
|
|
SETTING_GLOBAL_NOTIFICATIONS_DISABLED,
|
|
SETTING_MEDIA_SUPPORTED_MIMETYPES,
|
|
SETTING_MEDIA_CONTENTTYPES,
|
|
SETTING_COMMA_SEPARATED_FIELDS,
|
|
SETTING_REMOVE_QUOTES,
|
|
SETTING_CONFIG_DYNAMIC_FILE_PATH,
|
|
SETTING_PROJECTS,
|
|
SETTING_TAXONOMY_TAGS,
|
|
SETTING_TAXONOMY_CATEGORIES,
|
|
SETTING_CONTENT_FILTERS
|
|
} from '../constants';
|
|
import { Folders } from '../commands/Folders';
|
|
import { join, basename, dirname, parse } from 'path';
|
|
import { existsSync } from 'fs';
|
|
import { Extension } from './Extension';
|
|
import { debounceCallback } from './DebounceCallback';
|
|
import { Logger } from './Logger';
|
|
import * as jsoncParser from 'jsonc-parser';
|
|
import { existsAsync, fetchWithTimeout, readFileAsync, writeFileAsync } from '../utils';
|
|
import { Cache, Preview } from '../commands';
|
|
import { GitListener } from '../listeners/general';
|
|
import { DataListener } from '../listeners/panel';
|
|
import { MarkdownFoldingProvider } from '../providers/MarkdownFoldingProvider';
|
|
import { ModeSwitch } from '../services/ModeSwitch';
|
|
import * as l10n from '@vscode/l10n';
|
|
import { LocalizationKey } from '../localization';
|
|
|
|
export class Settings {
|
|
public static globalFile = 'frontmatter.json';
|
|
public static globalConfigFolder = '.frontmatter/config';
|
|
public static globalConfigPath: string | undefined = undefined;
|
|
public static globalConfig: any;
|
|
private static config: WorkspaceConfiguration;
|
|
private static isInitialized: boolean = false;
|
|
private static listeners: { id: string; callback: (global?: any) => void }[] = [];
|
|
private static fileCreationWatcher: FileSystemWatcher | undefined;
|
|
private static fileChangeWatcher: FileSystemWatcher | undefined;
|
|
private static fileSaveListener: Disposable;
|
|
private static fileDeleteListener: Disposable;
|
|
private static readConfigPromise: Promise<void> | undefined = undefined;
|
|
private static project: Project | undefined = undefined;
|
|
private static configDebouncer = debounceCallback();
|
|
|
|
public static async init() {
|
|
const allCommands = await commands.getCommands(true);
|
|
await Settings.readConfig();
|
|
|
|
const projects = Settings.getProjects();
|
|
const crntProject = await Extension.getInstance().getState<string | undefined>(
|
|
ExtensionState.Project.current,
|
|
'workspace'
|
|
);
|
|
|
|
if (projects.length > 0) {
|
|
// Get the default project
|
|
const defaultProject = projects.find((p) => {
|
|
if (crntProject) {
|
|
return p.name === crntProject;
|
|
}
|
|
return p.default;
|
|
});
|
|
if (defaultProject) {
|
|
Settings.project = defaultProject;
|
|
} else {
|
|
Settings.project = projects[0];
|
|
}
|
|
}
|
|
|
|
Settings.listeners = [];
|
|
|
|
if (!Settings.isInitialized) {
|
|
Settings.isInitialized = true;
|
|
|
|
if (!allCommands.includes(COMMAND_NAME.reloadConfig)) {
|
|
commands.registerCommand(COMMAND_NAME.reloadConfig, Settings.rebindWatchers);
|
|
}
|
|
}
|
|
|
|
if (!allCommands.includes(COMMAND_NAME.settingsRefresh)) {
|
|
commands.registerCommand(COMMAND_NAME.settingsRefresh, Settings.refreshConfig);
|
|
}
|
|
|
|
Settings.config = workspace.getConfiguration(CONFIG_KEY);
|
|
|
|
Settings.attachListener('settings-init', async () => {
|
|
Settings.config = workspace.getConfiguration(CONFIG_KEY);
|
|
});
|
|
|
|
Settings.onConfigChange();
|
|
}
|
|
|
|
/**
|
|
* Start listening to changes
|
|
*/
|
|
public static startListening() {
|
|
// Things to do when configuration changes
|
|
Settings.attachListener('settings-global', () => {
|
|
Preview.init();
|
|
GitListener.init();
|
|
|
|
DataListener.getFoldersAndFiles();
|
|
MarkdownFoldingProvider.triggerHighlighting(true);
|
|
ModeSwitch.register();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the current project
|
|
* @returns
|
|
*/
|
|
public static getProject() {
|
|
return Settings.project;
|
|
}
|
|
|
|
/**
|
|
* Set the project
|
|
* @param value
|
|
*/
|
|
public static setProject(value: string) {
|
|
Extension.getInstance().setState(ExtensionState.Project.current, value, 'workspace');
|
|
Settings.project = Settings.getProjects().find((p) => p.name === value);
|
|
}
|
|
|
|
/**
|
|
* Fetch all the projects
|
|
* @returns
|
|
*/
|
|
public static getProjects(): Project[] {
|
|
const settingKey = `${CONFIG_KEY}.${SETTING_PROJECTS}`;
|
|
|
|
let projects = [];
|
|
if (Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== 'undefined') {
|
|
projects = Settings.globalConfig[settingKey];
|
|
}
|
|
|
|
if (projects.length > 0) {
|
|
commands.executeCommand('setContext', CONTEXT.projectSwitchEnabled, true);
|
|
} else {
|
|
commands.executeCommand('setContext', CONTEXT.projectSwitchEnabled, false);
|
|
}
|
|
|
|
return projects;
|
|
}
|
|
|
|
/**
|
|
* Check if the setting is present in the workspace and ask to promote them to the global settings
|
|
*/
|
|
public static async checkToPromote() {
|
|
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(
|
|
ExtensionState.SettingPromoted,
|
|
'workspace'
|
|
);
|
|
if (!isPromoted) {
|
|
if (Settings.hasSettings()) {
|
|
window
|
|
.showInformationMessage(
|
|
l10n.t(LocalizationKey.helpersSettingsHelperCheckToPromoteMessage),
|
|
l10n.t(LocalizationKey.commonYes),
|
|
l10n.t(LocalizationKey.commonNo)
|
|
)
|
|
.then(async (result) => {
|
|
if (result === l10n.t(LocalizationKey.commonYes)) {
|
|
Settings.promote();
|
|
}
|
|
|
|
if (
|
|
result === l10n.t(LocalizationKey.commonNo) ||
|
|
result === l10n.t(LocalizationKey.commonYes)
|
|
) {
|
|
Extension.getInstance().setState(ExtensionState.SettingPromoted, true, 'workspace');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach a new listener for the setting changes
|
|
* @param id
|
|
* @param callback
|
|
* @returns
|
|
*/
|
|
public static attachListener(id: string, callback: (global?: any) => void) {
|
|
const listener = (Settings.listeners || []).find((l) => l.id === id);
|
|
if (listener) {
|
|
listener.callback = callback;
|
|
return;
|
|
}
|
|
|
|
if (!Settings.listeners) {
|
|
Settings.listeners = [];
|
|
}
|
|
|
|
Settings.listeners.push({
|
|
id,
|
|
callback
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger all the listeners
|
|
*/
|
|
public static triggerListeners() {
|
|
for (const listener of Settings.listeners || []) {
|
|
Logger.info(`Triggering listener: ${listener.id}`);
|
|
listener.callback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for config changes on global and local settings
|
|
* @param callback
|
|
*/
|
|
public static async onConfigChange() {
|
|
const projectConfig = await Settings.projectConfigPath();
|
|
|
|
workspace.onDidChangeConfiguration(() => {
|
|
Settings.triggerListeners();
|
|
});
|
|
|
|
if (projectConfig && !(await existsAsync(projectConfig))) {
|
|
// No config file, no need to watch
|
|
Settings.createFileCreationWatcher();
|
|
return;
|
|
}
|
|
|
|
// Background listener for when it is not a user interaction
|
|
if (projectConfig && existsSync(projectConfig)) {
|
|
if (Settings.fileChangeWatcher) {
|
|
Settings.fileChangeWatcher.dispose();
|
|
}
|
|
|
|
Settings.fileChangeWatcher = workspace.createFileSystemWatcher(
|
|
projectConfig,
|
|
true,
|
|
false,
|
|
true
|
|
);
|
|
Settings.fileChangeWatcher.onDidChange(async () => {
|
|
Logger.info(`Config change detected - ${projectConfig} changed`);
|
|
Settings.configDebouncer(() => Settings.triggerListeners(), 200);
|
|
});
|
|
}
|
|
|
|
if (Settings.fileSaveListener) {
|
|
Settings.fileSaveListener.dispose();
|
|
}
|
|
|
|
Settings.fileSaveListener = workspace.onDidSaveTextDocument(async (e) => {
|
|
const filename = e.uri.fsPath;
|
|
|
|
if (await Settings.checkProjectConfig(filename)) {
|
|
Logger.info(`Config change detected - ${filename} saved`);
|
|
await Settings.reloadConfig();
|
|
}
|
|
});
|
|
|
|
if (Settings.fileDeleteListener) {
|
|
Settings.fileDeleteListener.dispose();
|
|
}
|
|
|
|
Settings.fileDeleteListener = workspace.onDidDeleteFiles(async (e) => {
|
|
const needCallback = e?.files.find(async (f) => await Settings.checkProjectConfig(f.fsPath));
|
|
if (needCallback) {
|
|
await Settings.reloadConfig(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Inspect a setting
|
|
* @param name
|
|
* @returns
|
|
*/
|
|
public static inspect<T>(name: string): any {
|
|
const configInpection = Settings.config.inspect<T>(name);
|
|
const settingKey = `${CONFIG_KEY}.${name}`;
|
|
const teamValue =
|
|
Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== 'undefined'
|
|
? Settings.globalConfig[settingKey]
|
|
: undefined;
|
|
|
|
return {
|
|
...configInpection,
|
|
teamValue
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve a setting from global and local config
|
|
*/
|
|
public static get<T>(name: string, merging: boolean = false): T | undefined {
|
|
if (!Settings.config) {
|
|
return;
|
|
}
|
|
|
|
const configInpection = Settings.config.inspect<T>(name);
|
|
|
|
let setting = undefined;
|
|
const settingKey = `${CONFIG_KEY}.${name}`;
|
|
|
|
if (Settings.project) {
|
|
if (
|
|
typeof Settings.project.configuration !== 'undefined' &&
|
|
typeof Settings.project.configuration[settingKey] !== 'undefined'
|
|
) {
|
|
setting = Settings.project.configuration[settingKey];
|
|
return setting;
|
|
}
|
|
}
|
|
|
|
if (Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== 'undefined') {
|
|
setting = Settings.globalConfig[settingKey];
|
|
}
|
|
|
|
// Local overrides global
|
|
if (configInpection && typeof configInpection.workspaceValue !== 'undefined') {
|
|
if (merging && setting && typeof setting === 'object') {
|
|
setting = Object.assign([], setting, configInpection.workspaceValue);
|
|
} else {
|
|
setting = configInpection.workspaceValue;
|
|
}
|
|
}
|
|
|
|
if (setting === undefined) {
|
|
setting = Settings.config.get(name);
|
|
}
|
|
|
|
return setting;
|
|
}
|
|
|
|
/**
|
|
* String update config setting
|
|
* @param name
|
|
* @param value
|
|
*/
|
|
public static async update<T>(name: string, value: T, updateGlobal: boolean = false) {
|
|
const fmConfig = await Settings.projectConfigPath();
|
|
|
|
if (updateGlobal) {
|
|
if (fmConfig && (await existsAsync(fmConfig))) {
|
|
const localConfig = await readFileAsync(fmConfig, 'utf8');
|
|
Settings.globalConfig = jsoncParser.parse(localConfig);
|
|
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
|
|
|
|
const content = JSON.stringify(Settings.globalConfig, null, 2);
|
|
await writeFileAsync(fmConfig, content, 'utf8');
|
|
|
|
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
|
|
if (workspaceSettingValue) {
|
|
await Settings.update(name, undefined);
|
|
}
|
|
|
|
// Make sure to reload the whole config + all the data files
|
|
await Settings.readConfig();
|
|
|
|
return;
|
|
}
|
|
} else {
|
|
await Settings.config.update(name, value);
|
|
return;
|
|
}
|
|
|
|
// Fallback to the local settings
|
|
await Settings.config.update(name, value);
|
|
}
|
|
|
|
public static async remove(name: string) {
|
|
const fmConfig = await Settings.projectConfigPath();
|
|
|
|
if (fmConfig && (await existsAsync(fmConfig))) {
|
|
const localConfig = await readFileAsync(fmConfig, 'utf8');
|
|
Settings.globalConfig = jsoncParser.parse(localConfig);
|
|
delete Settings.globalConfig[`${CONFIG_KEY}.${name}`];
|
|
|
|
const content = JSON.stringify(Settings.globalConfig, null, 2);
|
|
await writeFileAsync(fmConfig, content, 'utf8');
|
|
|
|
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
|
|
if (workspaceSettingValue) {
|
|
await Settings.update(name, undefined);
|
|
}
|
|
|
|
// Make sure to reload the whole config + all the data files
|
|
await Settings.readConfig();
|
|
|
|
return;
|
|
}
|
|
|
|
await Settings.config.update(name, undefined);
|
|
}
|
|
|
|
/**
|
|
* Checks if the project contains the frontmatter.json file
|
|
*/
|
|
public static async hasProjectFile(): Promise<boolean> {
|
|
const wsFolder = Folders.getWorkspaceFolder();
|
|
if (!wsFolder) {
|
|
return false;
|
|
}
|
|
|
|
const globalConfigPath = await Settings.projectConfigPath();
|
|
if (!globalConfigPath) {
|
|
return false;
|
|
}
|
|
|
|
return await existsAsync(globalConfigPath);
|
|
}
|
|
|
|
/**
|
|
* Create team settings
|
|
*/
|
|
public static async createTeamSettings() {
|
|
const wsFolder = Folders.getWorkspaceFolder();
|
|
await Settings.createGlobalFile(wsFolder);
|
|
}
|
|
|
|
/**
|
|
* Create the frontmatter.json file
|
|
* @param wsFolder
|
|
*/
|
|
public static async createGlobalFile(wsFolder: Uri | undefined | null) {
|
|
const initialConfig = {
|
|
$schema: `https://${
|
|
Extension.getInstance().isBetaVersion() ? `beta.` : ``
|
|
}frontmatter.codes/frontmatter.schema.json`
|
|
};
|
|
|
|
if (wsFolder) {
|
|
const configPath = join(wsFolder.fsPath, Settings.globalFile);
|
|
if (!(await existsAsync(configPath))) {
|
|
await writeFileAsync(configPath, JSON.stringify(initialConfig, null, 2), 'utf8');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the taxonomy settings
|
|
*
|
|
* @param type
|
|
*/
|
|
public static getCustomTaxonomy(type: string): string[] {
|
|
const customTaxs = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true);
|
|
if (customTaxs && customTaxs.length > 0) {
|
|
return customTaxs.find((t) => t.id === type)?.options || [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Update the custom taxonomy settings
|
|
*
|
|
* @param config
|
|
* @param type
|
|
* @param options
|
|
*/
|
|
public static async updateCustomTaxonomy(id: string, option: string) {
|
|
const customTaxonomies = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true) || [];
|
|
let taxIdx = customTaxonomies?.findIndex((o) => o.id === id);
|
|
|
|
if (taxIdx === -1) {
|
|
customTaxonomies.push({
|
|
id,
|
|
options: []
|
|
} as CustomTaxonomy);
|
|
|
|
taxIdx = customTaxonomies?.findIndex((o) => o.id === id);
|
|
}
|
|
|
|
customTaxonomies[taxIdx].options.push(option);
|
|
customTaxonomies[taxIdx].options = [...new Set(customTaxonomies[taxIdx].options)];
|
|
customTaxonomies[taxIdx].options = customTaxonomies[taxIdx].options.sort().filter((o) => !!o);
|
|
await Settings.update(SETTING_TAXONOMY_CUSTOM, customTaxonomies, true);
|
|
}
|
|
|
|
/**
|
|
* Update the taxonomy settings
|
|
*
|
|
* @param type
|
|
* @param options
|
|
*/
|
|
public static async updateCustomTaxonomyOptions(id: string, options: string[]) {
|
|
const customTaxonomies = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true) || [];
|
|
let taxIdx = customTaxonomies?.findIndex((o) => o.id === id);
|
|
|
|
if (taxIdx !== -1) {
|
|
customTaxonomies[taxIdx].options = options;
|
|
}
|
|
|
|
await Settings.update(SETTING_TAXONOMY_CUSTOM, customTaxonomies, true);
|
|
}
|
|
|
|
/**
|
|
* Promote settings from local to team level
|
|
*/
|
|
public static async promote() {
|
|
const pkg = Extension.getInstance().packageJson;
|
|
if (pkg?.contributes?.configuration?.properties) {
|
|
const settingNames = Object.keys(pkg.contributes.configuration.properties);
|
|
|
|
for (const name of settingNames) {
|
|
const settingName = name.replace(`${CONFIG_KEY}.`, '');
|
|
const setting = Settings.config.inspect(settingName);
|
|
|
|
if (setting && typeof setting.workspaceValue !== 'undefined') {
|
|
await Settings.update(settingName, setting.workspaceValue, true);
|
|
await Settings.update(settingName, undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
Notifications.info(l10n.t(LocalizationKey.helpersSettingsHelperPromoteSuccess));
|
|
|
|
Telemetry.send(TelemetryEvent.promoteSettings);
|
|
}
|
|
|
|
/**
|
|
* Check if the setting is present in the workspace
|
|
* @param name
|
|
* @returns
|
|
*/
|
|
public static hasWorkspaceSettings<T>(name: string): T | undefined {
|
|
const setting = Settings.config.inspect<T>(name);
|
|
return setting && typeof setting.workspaceValue !== 'undefined'
|
|
? setting.workspaceValue
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if there are any Front Matter settings in the workspace
|
|
* @returns
|
|
*/
|
|
public static hasSettings() {
|
|
let hasSetting = false;
|
|
|
|
const pkg = Extension.getInstance().packageJson;
|
|
if (pkg?.contributes?.configuration?.properties) {
|
|
const settingNames = Object.keys(pkg.contributes.configuration.properties);
|
|
|
|
for (const name of settingNames) {
|
|
const settingName = name.replace(`${CONFIG_KEY}.`, '');
|
|
const setting = Settings.config.inspect(settingName);
|
|
|
|
if (setting && typeof setting.workspaceValue !== 'undefined') {
|
|
hasSetting = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hasSetting;
|
|
}
|
|
|
|
/**
|
|
* Get the project config path
|
|
* @returns
|
|
*/
|
|
public static async projectConfigPath() {
|
|
if (Settings.globalConfigPath) {
|
|
return Settings.globalConfigPath;
|
|
}
|
|
|
|
const rootFilePath = join(Folders.getWorkspaceFolder()?.fsPath || '', Settings.globalFile);
|
|
if (await existsAsync(rootFilePath)) {
|
|
Settings.globalConfigPath = rootFilePath;
|
|
return Settings.globalConfigPath;
|
|
}
|
|
|
|
let configFiles = await workspace.findFiles(`**/${Settings.globalFile}`, '**/node_modules/**');
|
|
// Sort by file path length
|
|
configFiles = configFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
|
|
|
|
if (configFiles.length === 0) {
|
|
Settings.globalConfigPath = undefined;
|
|
return Settings.globalConfigPath;
|
|
}
|
|
|
|
const configPath = configFiles[0].fsPath;
|
|
Settings.globalConfigPath = configPath;
|
|
|
|
return Settings.globalConfigPath;
|
|
}
|
|
|
|
/**
|
|
* Check if its the project config
|
|
* @param filePath
|
|
* @returns
|
|
*/
|
|
private static async checkProjectConfig(filePath: string) {
|
|
const fmConfig = await Settings.projectConfigPath();
|
|
filePath = parseWinPath(filePath);
|
|
|
|
if (filePath.includes(Settings.globalConfigFolder)) {
|
|
return true;
|
|
} else if (fmConfig && existsSync(fmConfig)) {
|
|
return (
|
|
filePath &&
|
|
basename(filePath).toLowerCase() === Settings.globalFile.toLowerCase() &&
|
|
fmConfig.toLowerCase() === filePath.toLowerCase()
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Read the global config file
|
|
*/
|
|
private static async readConfig() {
|
|
try {
|
|
const fmConfig = await Settings.projectConfigPath();
|
|
if (fmConfig && (await existsAsync(fmConfig))) {
|
|
const localConfig = await readFileAsync(fmConfig, 'utf8');
|
|
Settings.globalConfig = jsoncParser.parse(localConfig);
|
|
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
|
|
} else {
|
|
Settings.globalConfig = undefined;
|
|
}
|
|
|
|
// Check if the config got external configs
|
|
await Settings.processExternalConfig();
|
|
|
|
// Read the files from the config folder
|
|
let configFiles = await workspace.findFiles(
|
|
`**/${Settings.globalConfigFolder}/**/*.json`,
|
|
'**/node_modules/**'
|
|
);
|
|
if (configFiles.length === 0) {
|
|
Logger.info(`No ".frontmatter/config" config files found.`);
|
|
}
|
|
|
|
// Sort the files by fsPath
|
|
configFiles = configFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
|
|
for await (const configFile of configFiles) {
|
|
await Settings.processConfigFile(configFile);
|
|
}
|
|
|
|
// Check if there is a dynamic config file and use it to update the global config
|
|
if (
|
|
Settings.globalConfig &&
|
|
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CONFIG_DYNAMIC_FILE_PATH}`]
|
|
) {
|
|
const dynamicConfigPath =
|
|
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CONFIG_DYNAMIC_FILE_PATH}`];
|
|
if (dynamicConfigPath) {
|
|
try {
|
|
await window.withProgress(
|
|
{
|
|
location: ProgressLocation.Notification,
|
|
title: l10n.t(
|
|
LocalizationKey.helpersSettingsHelperReadConfigProgressTitle,
|
|
EXTENSION_NAME
|
|
)
|
|
},
|
|
async () => {
|
|
const absFilePath = Folders.getAbsFilePath(dynamicConfigPath);
|
|
Logger.info(`Reading dynamic config file: ${absFilePath}`);
|
|
if (absFilePath) {
|
|
if (await existsAsync(absFilePath)) {
|
|
const configFunction = require(absFilePath);
|
|
const dynamicConfig = await configFunction(
|
|
Object.assign({}, Settings.globalConfig)
|
|
);
|
|
|
|
if (dynamicConfig) {
|
|
Settings.globalConfig = dynamicConfig;
|
|
Logger.info(`Dynamic config file loaded`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
} catch (e) {
|
|
Logger.error(`Error reading dynamic config file: ${dynamicConfigPath}`);
|
|
Logger.error((e as Error).message);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Settings.globalConfig = undefined;
|
|
Notifications.errorWithOutput(l10n.t(LocalizationKey.helpersSettingsHelperReadConfigError));
|
|
Logger.error((e as Error).message);
|
|
}
|
|
|
|
Settings.readConfigPromise = undefined;
|
|
}
|
|
|
|
/**
|
|
* Process the external configs
|
|
*/
|
|
private static async processExternalConfig() {
|
|
const extendsConfigName = `${CONFIG_KEY}.${SETTING_EXTENDS}`;
|
|
if (!Settings.globalConfig || !Settings.globalConfig[extendsConfigName]) {
|
|
return;
|
|
}
|
|
|
|
const originalConfig = Object.assign({}, Settings.globalConfig);
|
|
const extendsConfig: string[] = Settings.globalConfig[extendsConfigName];
|
|
for (const externalConfig of extendsConfig) {
|
|
if (externalConfig.endsWith(`.json`)) {
|
|
const config = await Settings.getExternalConfig(externalConfig);
|
|
await Settings.extendConfig(config, originalConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the config file
|
|
* @param configFile
|
|
* @returns
|
|
*/
|
|
private static async processConfigFile(configFile: Uri) {
|
|
try {
|
|
const config = await workspace.fs.readFile(configFile);
|
|
const configJson = jsoncParser.parse(config.toString());
|
|
|
|
const filePath = parseWinPath(configFile.fsPath);
|
|
const configFilePath = filePath.split(Settings.globalConfigFolder).pop();
|
|
if (!configFilePath) {
|
|
return;
|
|
}
|
|
Logger.info(`Processing "${configFilePath}" config file.`);
|
|
|
|
// Get the path without the filename
|
|
const configFolder = parseWinPath(dirname(configFilePath));
|
|
let relSettingName = configFolder.split('/').join('.');
|
|
if (relSettingName.startsWith('.')) {
|
|
relSettingName = relSettingName.substring(1);
|
|
}
|
|
relSettingName = relSettingName.toLowerCase();
|
|
|
|
if (!Settings.globalConfig) {
|
|
Settings.globalConfig = {};
|
|
}
|
|
|
|
Settings.updateGlobalConfigSetting(relSettingName, configJson, configFilePath, filePath);
|
|
} catch (e) {
|
|
Logger.error(`Error reading config file: ${configFile.fsPath}`);
|
|
Logger.error((e as Error).message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extend the config with external config data
|
|
* @param config
|
|
* @param originalConfig The original config data is used to make sure we don't override settings coming from the fontmatter.json file.
|
|
* @returns
|
|
*/
|
|
private static async extendConfig(config: any, originalConfig: any) {
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
// We need to loop through the config to make sure the objects and arrays are merged
|
|
for (const key in config) {
|
|
if (config.hasOwnProperty(key)) {
|
|
const value = config[key];
|
|
const settingName = key.replace(`${CONFIG_KEY}.`, '');
|
|
|
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
if (typeof originalConfig[key] === 'undefined') {
|
|
Settings.globalConfig[key] = value;
|
|
}
|
|
}
|
|
// Objects and arrays to override
|
|
else if (
|
|
settingName === SETTING_CONTENT_DRAFT_FIELD ||
|
|
settingName === SETTING_CONTENT_SUPPORTED_FILETYPES ||
|
|
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
|
|
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
|
|
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
|
|
settingName === SETTING_COMMA_SEPARATED_FIELDS ||
|
|
settingName === SETTING_CONTENT_FILTERS
|
|
) {
|
|
if (typeof originalConfig[key] === 'undefined') {
|
|
Settings.globalConfig[key] = value;
|
|
}
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
// Check if array
|
|
if (Array.isArray(value)) {
|
|
if (
|
|
settingName === SETTING_TAXONOMY_CATEGORIES ||
|
|
settingName === SETTING_TAXONOMY_TAGS ||
|
|
settingName === SETTING_REMOVE_QUOTES
|
|
) {
|
|
// Merge the arrays
|
|
Settings.globalConfig[key] = [
|
|
...(Settings.globalConfig[key] || []),
|
|
...(originalConfig[key] || []),
|
|
...value
|
|
];
|
|
// Filter out the doubles
|
|
Settings.globalConfig[key] = Settings.globalConfig[key].filter(
|
|
(item: any, index: number) => {
|
|
return Settings.globalConfig[key].indexOf(item) === index;
|
|
},
|
|
Settings.globalConfig[key]
|
|
);
|
|
} else {
|
|
for (const item of value) {
|
|
Settings.updateGlobalConfigSetting(settingName, item);
|
|
}
|
|
}
|
|
} else if (settingName === SETTING_CONTENT_SNIPPETS) {
|
|
for (const itemKey in value) {
|
|
const crntValue = Settings.globalConfig[key] || {};
|
|
|
|
if (!crntValue[itemKey]) {
|
|
Settings.globalConfig[key] = {
|
|
...crntValue,
|
|
...{ [itemKey]: value[itemKey] }
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the global config array/object settings
|
|
* @param relSettingName
|
|
* @param configJson
|
|
*/
|
|
private static updateGlobalConfigSetting<T>(
|
|
relSettingName: string,
|
|
configJson: any,
|
|
configFilePath?: string,
|
|
filePath?: string
|
|
): void {
|
|
// Custom scripts
|
|
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
|
|
// const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
|
|
// Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
|
|
Settings.updateGlobalConfigArraySetting(SETTING_CUSTOM_SCRIPTS, 'id', configJson, 'script');
|
|
}
|
|
// Content types
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, 'name', configJson);
|
|
}
|
|
// Media Content types
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_MEDIA_CONTENTTYPES)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_MEDIA_CONTENTTYPES, 'name', configJson);
|
|
}
|
|
// Data files
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, 'id', configJson);
|
|
}
|
|
// Data folders
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FOLDERS)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FOLDERS, 'id', configJson);
|
|
}
|
|
// Data types
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_TYPES)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_DATA_TYPES, 'id', configJson);
|
|
}
|
|
// Page folders
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PAGE_FOLDERS)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PAGE_FOLDERS, 'path', {
|
|
...configJson,
|
|
extended: true
|
|
});
|
|
}
|
|
// Placeholders
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PLACEHOLDERS)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PLACEHOLDERS, 'id', configJson);
|
|
}
|
|
// Sorting
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SORTING)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_SORTING, 'id', configJson);
|
|
}
|
|
// Modes
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_GLOBAL_MODES)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_GLOBAL_MODES, 'id', configJson);
|
|
}
|
|
// Field groups
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_FIELD_GROUPS)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_FIELD_GROUPS, 'id', configJson);
|
|
}
|
|
// Custom taxonomy
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CUSTOM)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CUSTOM, 'id', configJson);
|
|
}
|
|
// Projects
|
|
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_PROJECTS)) {
|
|
Settings.updateGlobalConfigArraySetting(SETTING_PROJECTS, 'name', configJson);
|
|
}
|
|
// Snippets
|
|
else if (
|
|
Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS) &&
|
|
configFilePath &&
|
|
filePath
|
|
) {
|
|
Settings.updateGlobalConfigObjectByNameSetting(
|
|
SETTING_CONTENT_SNIPPETS,
|
|
configFilePath,
|
|
configJson,
|
|
filePath
|
|
);
|
|
} else if (typeof configJson === 'string') {
|
|
Settings.mergeStringArray(`${CONFIG_KEY}.${relSettingName}`, configJson);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge the array setting in the global config
|
|
* @param key
|
|
* @param value
|
|
*/
|
|
private static mergeStringArray(key: string, value: string) {
|
|
// Merge the arrays
|
|
Settings.globalConfig[key] = [...(Settings.globalConfig[key] || []), value];
|
|
Settings.globalConfig[key] = Settings.globalConfig[key].filter((item: any, index: number) => {
|
|
return Settings.globalConfig[key].indexOf(item) === index;
|
|
}, Settings.globalConfig[key]);
|
|
}
|
|
|
|
/**
|
|
* Check if the setting name is equal or starts with the reference setting name
|
|
* @param value
|
|
* @param startsWith
|
|
* @returns
|
|
*/
|
|
private static isEqualOrStartsWith(value: string, startsWith: string) {
|
|
value = value.toLowerCase();
|
|
startsWith = startsWith.toLowerCase();
|
|
|
|
return value === startsWith || value.startsWith(`${startsWith}.`);
|
|
}
|
|
|
|
/**
|
|
* Update an array setting in the global config
|
|
* @param settingName
|
|
* @param fieldName
|
|
* @param configJson
|
|
*/
|
|
private static updateGlobalConfigArraySetting<T>(
|
|
settingName: string,
|
|
fieldName: string,
|
|
configJson: any,
|
|
fallbackFieldName?: string
|
|
): void {
|
|
const crntValue: T[] = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || [];
|
|
|
|
const itemIdx = crntValue.findIndex((item: any) => {
|
|
if (typeof item[fieldName] !== 'undefined') {
|
|
return item[fieldName] === configJson[fieldName];
|
|
} else if (fallbackFieldName && typeof item[fallbackFieldName] !== 'undefined') {
|
|
return item[fallbackFieldName] === configJson[fallbackFieldName];
|
|
} else {
|
|
return false;
|
|
}
|
|
});
|
|
if (itemIdx === -1) {
|
|
crntValue.push(configJson);
|
|
}
|
|
|
|
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = [...crntValue];
|
|
}
|
|
|
|
/**
|
|
* Update an object by the file name in the global config
|
|
* @param settingName
|
|
* @param fileNamepath
|
|
* @param configJson
|
|
*/
|
|
private static updateGlobalConfigObjectByNameSetting<T>(
|
|
settingName: string,
|
|
fileNamepath: string,
|
|
configJson: any,
|
|
absPath: string
|
|
): void {
|
|
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || {};
|
|
|
|
// Filename is the key
|
|
const fileName = parse(fileNamepath).name;
|
|
|
|
configJson = {
|
|
...configJson,
|
|
sourcePath: absPath
|
|
};
|
|
|
|
if (!crntValue[fileName]) {
|
|
crntValue[fileName] = configJson;
|
|
|
|
Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] = {
|
|
...crntValue,
|
|
...{ [fileName]: configJson }
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a file creation watcher
|
|
*/
|
|
private static createFileCreationWatcher() {
|
|
const ext = Extension.getInstance();
|
|
|
|
if (!Settings.fileCreationWatcher) {
|
|
Settings.fileCreationWatcher = workspace.createFileSystemWatcher(
|
|
`**/*.json`,
|
|
false,
|
|
true,
|
|
true
|
|
);
|
|
Settings.fileCreationWatcher.onDidCreate(
|
|
async (uri) => {
|
|
const globalConfigPath = await Settings.projectConfigPath();
|
|
if (globalConfigPath && parseWinPath(uri.fsPath) === parseWinPath(globalConfigPath)) {
|
|
Settings.onConfigChange();
|
|
Settings.rebindWatchers();
|
|
// Stop listening to file creation events
|
|
Settings.fileCreationWatcher?.dispose();
|
|
Settings.fileCreationWatcher = undefined;
|
|
}
|
|
},
|
|
null,
|
|
ext.subscriptions
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebind the configuration watchers
|
|
*/
|
|
private static rebindWatchers() {
|
|
Logger.info(`Rebinding ${(Settings.listeners || []).length} listeners`);
|
|
|
|
(Settings.listeners || []).forEach((l) => {
|
|
Settings.attachListener(l.id, l.callback);
|
|
});
|
|
|
|
Settings.triggerListeners();
|
|
}
|
|
|
|
/**
|
|
* Retrieve the external configuration
|
|
* @param configPath
|
|
* @returns
|
|
*/
|
|
private static async getExternalConfig(configPath: string): Promise<any> {
|
|
let config: any = undefined;
|
|
|
|
if (configPath.startsWith('https://')) {
|
|
try {
|
|
let cachedResponse = await Cache.get<{
|
|
[config: string]: { expires: number; data: any };
|
|
}>(ExtensionState.Settings.Extends, 'workspace');
|
|
|
|
if (
|
|
cachedResponse &&
|
|
cachedResponse[configPath] &&
|
|
cachedResponse[configPath].expires > new Date().getTime()
|
|
) {
|
|
config = cachedResponse[configPath].data;
|
|
} else {
|
|
const response = await fetchWithTimeout(configPath, { method: 'GET' });
|
|
if (response.ok) {
|
|
config = await response.json();
|
|
|
|
if (!cachedResponse) {
|
|
cachedResponse = {};
|
|
}
|
|
|
|
cachedResponse[configPath] = {
|
|
expires: new Date(new Date().getTime() + 1000 * 60 * 10).getTime(),
|
|
data: config
|
|
};
|
|
|
|
await Cache.set(ExtensionState.Settings.Extends, cachedResponse, 'workspace');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Logger.error(`Error fetching external config "${configPath}".`);
|
|
}
|
|
} else {
|
|
const absConfigPath = join(Folders.getWorkspaceFolder()?.fsPath || '', configPath);
|
|
if (await existsAsync(absConfigPath)) {
|
|
const configTxt = await readFileAsync(absConfigPath, 'utf8');
|
|
config = jsoncParser.parse(configTxt);
|
|
} else {
|
|
Logger.error(`External config "${configPath}" not found.`);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Reload the config
|
|
* @param debounced
|
|
*/
|
|
private static async refreshConfig() {
|
|
await Settings.reloadConfig();
|
|
Notifications.info(l10n.t(LocalizationKey.helpersSettingsHelperRefreshConfigSuccess));
|
|
}
|
|
|
|
/**
|
|
* Reload the config
|
|
* @param debounced
|
|
*/
|
|
private static async reloadConfig(debounced: boolean = true) {
|
|
Logger.info(`Reloading config...`);
|
|
if (Settings.readConfigPromise === undefined) {
|
|
Settings.readConfigPromise = Settings.readConfig();
|
|
}
|
|
await Settings.readConfigPromise;
|
|
|
|
Logger.info(`Reloaded config...`);
|
|
if (debounced) {
|
|
Settings.configDebouncer(() => Settings.triggerListeners(), 200);
|
|
} else {
|
|
Settings.triggerListeners();
|
|
}
|
|
}
|
|
}
|