Skip to content

Commit

Permalink
Merge pull request #1192 from pjkaufman/master
Browse files Browse the repository at this point in the history
Handle Rule Conflicts
  • Loading branch information
pjkaufman authored Oct 14, 2024
2 parents 879ed9b + 6c27734 commit 145463a
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 71 deletions.
4 changes: 3 additions & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const mockedPlugins = [replace({
// update usage of moment from obsidian to the node implementation of moment we have
'import {moment} from \'obsidian\';': 'import moment from \'moment\';',
// remove the use of obsidian in the options to allow for docs.js to run
'import {Setting} from \'obsidian\';': '',
'import {App, Setting, ToggleComponent} from \'obsidian\';': '',
// remove the use of obsidian in settings helper to allow for docs.js to run
'import {App, MarkdownRenderer} from \'obsidian\';': '',
// remove the use of obsidian in the auto-correct files picker to allow for docs.js to run
Expand All @@ -38,6 +38,8 @@ const mockedPlugins = [replace({
'import {App, TFile} from \'obsidian\';': '',
// remove the use of obsidian in parse results modal to allow for docs.js to run
'import {Modal, App} from \'obsidian\';': 'class Modal {}',
// remove the use of app from a couple of settings for docs.js to run
'import {App} from \'obsidian\';': '',
},
delimiters: ['', ''],
})];
Expand Down
6 changes: 6 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export default {

'copy-aria-label': 'Copy',

'disabled-other-rule-notice': 'If you enable <code>{NAME_1}</code>, it will disable <code>{NAME_2}</code>. Would you like to proceed?',
'disabled-conflicting-rule-notice': '{NAME_1}, conflicts with {NAME_2}, so it has been turned off. You can switch which setting is off in the settings tab.',

// confirm-rule-disable-modal.ts
'ok': 'Ok',

// parse-results-modal.ts
'parse-results-heading-text': 'Custom Parse Values',
'file-parse-description-text': 'The following is the list of custom replacements found in {FILE}.',
Expand Down
170 changes: 111 additions & 59 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,69 +129,13 @@ export default class LinterPlugin extends Plugin {
setLogLevel(this.settings.logLevel);
await this.setOrUpdateMomentInstance();

let updateMade = false;
if (!this.settings.settingsConvertedToConfigKeyValues) {
updateMade = await this.moveConfigValuesToKeyBasedFormat();
}

if ('lintOnFileContentChangeDelay' in this.settings) {
this.settings.ruleConfigs['yaml-timestamp']['update-on-file-contents-updated'] = this.settings['lintOnFileContentChangeDelay'];

delete this.settings['lintOnFileContentChangeDelay'];
updateMade = true;
}

// make sure to load the defaults of any missing rules to make sure they do not cause issues on the settings page
for (const rule of rules) {
if (!this.settings.ruleConfigs[rule.alias]) {
this.settings.ruleConfigs[rule.alias] = rule.getDefaultOptions();
}

// remove this after a reasonable amount of time
if (rule.alias == 'space-between-chinese-japanese-or-korean-and-english-or-numbers') {
const defaults = rule.getDefaultOptions();
if (!('english-symbols-punctuation-before' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['english-symbols-punctuation-before'] = defaults['english-symbols-punctuation-before'];
updateMade = true;
}

if (!('english-symbols-punctuation-after' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['english-symbols-punctuation-after'] = defaults['english-symbols-punctuation-after'];
updateMade = true;
}
} else if (rule.alias == 'yaml-timestamp') {
const defaults = rule.getDefaultOptions();
if ('force-retention-of-create-value' in this.settings.ruleConfigs[rule.alias]) {
if (!('date-created-source-of-truth' in this.settings.ruleConfigs[rule.alias])) {
if (this.settings.ruleConfigs[rule.alias]['force-retention-of-create-value']) {
this.settings.ruleConfigs[rule.alias]['date-created-source-of-truth'] = 'frontmatter';
} else {
this.settings.ruleConfigs[rule.alias]['date-created-source-of-truth'] = defaults['date-created-source-of-truth'];
}
}

delete this.settings.ruleConfigs[rule.alias]['force-retention-of-create-value'];
updateMade = true;
}

if (!('date-modified-source-of-truth' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['date-modified-source-of-truth'] = defaults['date-modified-source-of-truth'];
updateMade = true;
}
}
}

this.updatePasteOverrideStatus();
this.updateHasCustomCommandStatus();

if (updateMade) {
await this.saveSettings();
}
}

async saveSettings() {
if (!this.hasLoadedMisspellingFiles) {
await this.loadAutoCorrectFiles();
await this.loadAutoCorrectFiles(false);
}

await this.saveData(this.settings);
Expand Down Expand Up @@ -328,7 +272,8 @@ export default class LinterPlugin extends Plugin {
this.eventRefs.push(eventRef);

this.app.workspace.onLayoutReady(async () => {
await this.loadAutoCorrectFiles();
await this.makeSureSettingsFilledInAndCleanupSettings();
await this.loadAutoCorrectFiles(true);
});

// Source for save setting
Expand Down Expand Up @@ -375,13 +320,17 @@ export default class LinterPlugin extends Plugin {
}
}

async loadAutoCorrectFiles() {
async loadAutoCorrectFiles(isOnload: boolean) {
const customAutoCorrectSettings = this.settings.ruleConfigs['auto-correct-common-misspellings'];
if (!customAutoCorrectSettings || !customAutoCorrectSettings.enabled) {
return;
}

await downloadMisspellings(this, async (message: string) => {
if (isOnload) {
message = 'Obsidian Linter:\n' + message;
}

new Notice(message);

this.settings.ruleConfigs['auto-correct-common-misspellings'].enabled = false;
Expand Down Expand Up @@ -619,6 +568,109 @@ export default class LinterPlugin extends Plugin {
moment.locale(oldLocale);
}

private async makeSureSettingsFilledInAndCleanupSettings() {
let updateMade = false;

// migrate settings over to the new format if they are using the now deprecated format that uses
// actual settings names for the key in the json
if (!this.settings.settingsConvertedToConfigKeyValues) {
updateMade = await this.moveConfigValuesToKeyBasedFormat();
}

// move a recently moved setting to its new location
if ('lintOnFileContentChangeDelay' in this.settings) {
this.settings.ruleConfigs['yaml-timestamp']['update-on-file-contents-updated'] = this.settings['lintOnFileContentChangeDelay'];

delete this.settings['lintOnFileContentChangeDelay'];
updateMade = true;
}

// check for and fix invalid settings
let noticeText = 'Obsidian Linter:';
let conflictingRulePresent = false;
if (this.settings.ruleConfigs['header-increment'] && this.settings.ruleConfigs['header-increment'].enabled && this.settings.ruleConfigs['header-increment']['start-at-h2'] &&
this.settings.ruleConfigs['file-name-heading'] && this.settings.ruleConfigs['file-name-heading'].enabled
) {
this.settings.ruleConfigs['header-increment']['start-at-h2'] = false;
updateMade = true;
conflictingRulePresent = true;

noticeText += '\n' + getTextInLanguage('disabled-conflicting-rule-notice').replace('{NAME_1}', getTextInLanguage('rules.header-increment.start-at-h2.name')).replace('{NAME_2}', getTextInLanguage('rules.file-name-heading.name'));
}

if (this.settings.ruleConfigs['paragraph-blank-lines'] && this.settings.ruleConfigs['paragraph-blank-lines'].enabled &&
this.settings.ruleConfigs['two-spaces-between-lines-with-content'] && this.settings.ruleConfigs['two-spaces-between-lines-with-content'].enabled
) {
this.settings.ruleConfigs['paragraph-blank-lines'].enabled = false;
updateMade = true;

if (conflictingRulePresent) {
noticeText += '\n';
}
conflictingRulePresent = true;

noticeText += '\n' + getTextInLanguage('disabled-conflicting-rule-notice').replace('{NAME_1}', getTextInLanguage('rules.paragraph-blank-lines.name')).replace('{NAME_2}', getTextInLanguage('rules.two-spaces-between-lines-with-content.name'));
}

if (conflictingRulePresent) {
new Notice(noticeText, userClickTimeout);
}

// make sure to load the defaults of any missing rules to make sure they do not cause issues on the settings page
for (const rule of rules) {
const ruleDefaults = rule.getDefaultOptions();
if (!this.settings.ruleConfigs[rule.alias]) {
this.settings.ruleConfigs[rule.alias] = ruleDefaults;
updateMade = true;
continue;
}

// remove this after a reasonable amount of time
if (rule.alias == 'space-between-chinese-japanese-or-korean-and-english-or-numbers') {
if (!('english-symbols-punctuation-before' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['english-symbols-punctuation-before'] = ruleDefaults['english-symbols-punctuation-before'];
updateMade = true;
}

if (!('english-symbols-punctuation-after' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['english-symbols-punctuation-after'] = ruleDefaults['english-symbols-punctuation-after'];
updateMade = true;
}
} else if (rule.alias == 'yaml-timestamp') {
const defaults = rule.getDefaultOptions();
if ('force-retention-of-create-value' in this.settings.ruleConfigs[rule.alias]) {
if (!('date-created-source-of-truth' in this.settings.ruleConfigs[rule.alias])) {
if (this.settings.ruleConfigs[rule.alias]['force-retention-of-create-value']) {
this.settings.ruleConfigs[rule.alias]['date-created-source-of-truth'] = 'frontmatter';
} else {
this.settings.ruleConfigs[rule.alias]['date-created-source-of-truth'] = defaults['date-created-source-of-truth'];
}
}

delete this.settings.ruleConfigs[rule.alias]['force-retention-of-create-value'];
updateMade = true;
}

if (!('date-modified-source-of-truth' in this.settings.ruleConfigs[rule.alias])) {
this.settings.ruleConfigs[rule.alias]['date-modified-source-of-truth'] = defaults['date-modified-source-of-truth'];
updateMade = true;
}
}

// make sure new/empty settings on a rule that exists get filled in with their default value as well
for (const key of Object.keys(ruleDefaults)) {
if (!Object.hasOwn(this.settings.ruleConfigs[rule.alias], key)) {
this.settings.ruleConfigs[rule.alias][key] = ruleDefaults[key];
updateMade = true;
}
}
}

if (updateMade) {
await this.saveSettings();
}
}

private createDebouncedFileUpdate(): Debouncer<[TFile, Editor], Promise<void>> {
let delay = 5000;
switch (this.settings.ruleConfigs['yaml-timestamp']['update-on-file-contents-updated'] ?? AfterFileChangeLintTimes.Never) {
Expand Down
17 changes: 14 additions & 3 deletions src/option.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Setting} from 'obsidian';
import {App, Setting, ToggleComponent} from 'obsidian';
import {getTextInLanguage, LanguageStringKey} from './lang/helpers';
import LinterPlugin from './main';
import {hideEl, unhideEl, setElContent} from './ui/helpers';
Expand Down Expand Up @@ -63,21 +63,24 @@ export abstract class Option {

export class BooleanOption extends Option {
public defaultValue: boolean;
private toggleComponent: ToggleComponent;

constructor(configKey: string, nameKey: LanguageStringKey, descriptionKey: LanguageStringKey, defaultValue: any, ruleAlias?: string | null, private onChange?: (value: boolean) => void) {
constructor(configKey: string, nameKey: LanguageStringKey, descriptionKey: LanguageStringKey, defaultValue: any, ruleAlias?: string | null, private onChange?: (value: boolean, app: App) => void) {
super(configKey, nameKey, descriptionKey, defaultValue, ruleAlias);
}

public display(containerEl: HTMLElement, settings: LinterSettings, plugin: LinterPlugin): void {
this.setting = new Setting(containerEl)
.addToggle((toggle) => {
this.toggleComponent = toggle;

toggle.setValue(settings.ruleConfigs[this.ruleAlias][this.configKey]);
toggle.onChange((value) => {
this.setOption(value, settings);
plugin.settings = settings;

if (this.onChange) {
this.onChange(value);
this.onChange(value, plugin.app);
}

void plugin.saveSettings();
Expand All @@ -86,6 +89,14 @@ export class BooleanOption extends Option {

this.parseNameAndDescriptionAndRemoveSettingBorder();
}

getValue(): boolean {
return this.toggleComponent.getValue();
}

setValue(value: boolean) {
this.toggleComponent.setValue(value);
}
}

export class TextOption extends Option {
Expand Down
9 changes: 8 additions & 1 deletion src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {LinterError} from './linter-error';
import {getTextInLanguage, LanguageStringKey} from './lang/helpers';
import {ignoreListOfTypes, IgnoreType} from './utils/ignore-types';
import {LinterSettings} from './settings-data';
import {App} from 'obsidian';

export type Options = { [optionName: string]: any};

Expand Down Expand Up @@ -41,6 +42,7 @@ export class Rule {
* @param {Array<Option>} [options=[]] - The options of the rule to be displayed in the documentation
* @param {boolean} [hasSpecialExecutionOrder=false] - The rule has special execution order
* @param {IgnoreType[]} [ignoreTypes=[]] - The types of elements to ignore for the rule
* @param {function(boolean):boolean} [disableConflictingOptions=null] - The function to disable conflicting rules or options when it is enabled
*/
constructor(
private nameKey: LanguageStringKey,
Expand All @@ -53,10 +55,15 @@ export class Rule {
public options: Array<Option> = [],
public readonly hasSpecialExecutionOrder: boolean = false,
public readonly ignoreTypes: IgnoreType[] = [],
disableConflictingOptions: (value: boolean, app: App) => void = null,
) {
this.ruleHeading = this.getName().toLowerCase().replaceAll(' ', '-');

options.unshift(new BooleanOption('enabled', this.descriptionKey, '' as LanguageStringKey, false, alias, (value: boolean) => {
options.unshift(new BooleanOption('enabled', this.descriptionKey, '' as LanguageStringKey, false, alias, (value: boolean, app: App) => {
if (value && disableConflictingOptions) {
disableConflictingOptions(value, app);
}

if (options.length > 1) {
for (let i = 1; i < options.length; i++) {
if (value) {
Expand Down
18 changes: 17 additions & 1 deletion src/rules/file-name-heading.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {Options, RuleType} from '../rules';
import {Options, rulesDict, RuleType} from '../rules';
import RuleBuilder, {ExampleBuilder, OptionBuilderBase} from './rule-builder';
import dedent from 'ts-dedent';
import {IgnoreTypes} from '../utils/ignore-types';
import {insert} from '../utils/strings';
import {App} from 'obsidian';
import {BooleanOption} from '../option';
import {ConfirmRuleDisableModal} from '../ui/modals/confirm-rule-disable-modal';

class FileNameHeadingOptions implements Options {
@RuleBuilder.noSettingControl()
Expand All @@ -17,6 +20,19 @@ export default class FileNameHeading extends RuleBuilder<FileNameHeadingOptions>
descriptionKey: 'rules.file-name-heading.description',
type: RuleType.HEADING,
ruleIgnoreTypes: [IgnoreTypes.code, IgnoreTypes.math, IgnoreTypes.yaml, IgnoreTypes.link, IgnoreTypes.wikiLink, IgnoreTypes.tag],
disableConflictingOptions(value: boolean, app: App): void {
const headerIncrementOptions = rulesDict['header-increment'];
const headerIncrementEnableOption = headerIncrementOptions.options[0] as BooleanOption;
const headerIncrementStartAtH2Option = headerIncrementOptions.options[1] as BooleanOption;
if (value && headerIncrementEnableOption.getValue()) {
new ConfirmRuleDisableModal(app, 'rules.file-name-heading.name', 'rules.header-increment.start-at-h2.name', () => {
headerIncrementStartAtH2Option.setValue(false);
},
() => {
(rulesDict['file-name-heading'].options[0] as BooleanOption).setValue(false);
}).open();
}
},
});
}
get OptionsClass(): new () => FileNameHeadingOptions {
Expand Down
17 changes: 16 additions & 1 deletion src/rules/header-increment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {Options, RuleType} from '../rules';
import {Options, rulesDict, RuleType} from '../rules';
import RuleBuilder, {BooleanOptionBuilder, ExampleBuilder, OptionBuilderBase} from './rule-builder';
import dedent from 'ts-dedent';
import {IgnoreTypes} from '../utils/ignore-types';
import {allHeadersRegex} from '../utils/regex';
import {BooleanOption} from '../option';
import {ConfirmRuleDisableModal} from '../ui/modals/confirm-rule-disable-modal';
import {App} from 'obsidian';

class HeaderIncrementOptions implements Options {
startAtH2?: boolean = false;
Expand Down Expand Up @@ -169,6 +172,18 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
nameKey: 'rules.header-increment.start-at-h2.name',
descriptionKey: 'rules.header-increment.start-at-h2.description',
optionsKey: 'startAtH2',
onChange(value: boolean, app: App): void {
const filenameHeadingEnableOption = rulesDict['file-name-heading'].options[0] as BooleanOption;

if (value && filenameHeadingEnableOption.getValue()) {
new ConfirmRuleDisableModal(app, 'rules.header-increment.start-at-h2.name', 'rules.file-name-heading.name', () => {
filenameHeadingEnableOption.setValue(false);
},
() => {
(rulesDict['header-increment'].options[1] as BooleanOption).setValue(false);
}).open();
}
},
}),
];
}
Expand Down
Loading

0 comments on commit 145463a

Please sign in to comment.