diff --git a/src/cm6/rule-alias-suggester.ts b/src/cm6/rule-alias-suggester.ts new file mode 100644 index 00000000..a4fc3392 --- /dev/null +++ b/src/cm6/rule-alias-suggester.ts @@ -0,0 +1,111 @@ +import {Editor, EditorPosition, EditorSuggest, EditorSuggestContext, EditorSuggestTriggerInfo, TFile} from 'obsidian'; +import LinterPlugin from '../main'; +import {getDisabledRules, rules} from '../rules'; +import {DISABLED_RULES_KEY, getYamlSectionValue} from '../utils/yaml'; +import {getTextInLanguage} from '../lang/helpers'; + +const openingYAMLIndicator = /^---\n/gm; +const disableRulesKeyWithColon = `${DISABLED_RULES_KEY}:`; + +export type ruleInfo = { + displayName: string, + name: string, + alias: string +} + +// based on tag suggester, see https://github.com/jmilldotdev/obsidian-frontmatter-tag-suggest/blob/d80bcfb64d96d7fcb908deb5f4b0c9c8041c267c/main.ts +export class RuleAliasSuggest extends EditorSuggest { + ruleInfo: ruleInfo[]; + + constructor(public plugin: LinterPlugin) { + super(plugin.app); + + const allName = getTextInLanguage('all-rules-option'); + this.ruleInfo = [{displayName: allName, name: allName.toLowerCase(), alias: 'all'}]; + for (const rule of rules) { + const name = rule.getName(); + this.ruleInfo.push({displayName: name, name: name.toLowerCase(), alias: rule.alias}); + } + } + inline = false; + onTrigger(cursor: EditorPosition, editor: Editor, _: TFile): EditorSuggestTriggerInfo | null { + const lineContents = editor.getLine(cursor.line).toLowerCase(); + const onFrontmatterDisabledRulesLine = lineContents.startsWith(disableRulesKeyWithColon) || + this.disabledRulesIsEndOfStartOfFileToCursor(editor.getRange({line: 0, ch: 0}, cursor)); + + if (onFrontmatterDisabledRulesLine) { + this.inline = lineContents.startsWith(disableRulesKeyWithColon); + const sub = editor.getLine(cursor.line).substring(0, cursor.ch); + const match = sub.match(/(\S+)$/)?.first().replaceAll('[', '').replaceAll(']', ''); + if (match) { + const matchData = { + end: cursor, + start: { + ch: sub.lastIndexOf(match), + line: cursor.line, + }, + query: match, + }; + return matchData; + } + } + + return null; + } + + getSuggestions(context: EditorSuggestContext): ruleInfo[] { + const [disabledRules, allIncluded]= getDisabledRules(context.editor.getValue()); + if (allIncluded) { + return []; + } + + const query = context.query.toLowerCase(); + const suggestions = this.ruleInfo.filter((r: ruleInfo) => + (r.name.contains(query) || r.alias.contains(query)) && !disabledRules.includes(r.alias), + ); + + return suggestions; + } + + renderSuggestion(suggestion: ruleInfo, el: HTMLElement): void { + el.addClass('mod-complex'); + + const outer = el.createDiv({cls: 'suggestion-content'}); + outer.createDiv({cls: 'suggestion-title'}).setText(`${suggestion.displayName}`); + outer.createDiv({cls: 'suggestion-note'}).setText(`${suggestion.alias}`); + } + + selectSuggestion(suggestion: ruleInfo): void { + if (this.context) { + let suggestedValue = suggestion.alias; + if (this.inline) { + suggestedValue = `${suggestedValue},`; + } else { + suggestedValue = `${suggestedValue}\n -`; + } + + (this.context.editor as Editor).replaceRange( + `${suggestedValue} `, + this.context.start, + this.context.end, + ); + } + } + + disabledRulesIsEndOfStartOfFileToCursor(range: string): boolean { + if (!range || !range.length) { + return false; + } + + if (range.match(openingYAMLIndicator)?.length != 1) { + return false; + } + + const disabledRules = getYamlSectionValue(range + '\n', DISABLED_RULES_KEY)?.trimEnd(); + if (disabledRules === null) { + return false; + } + + return range.trimEnd().endsWith(disabledRules); + } +} diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts index dc0f7816..4049bf5c 100644 --- a/src/lang/locale/de.ts +++ b/src/lang/locale/de.ts @@ -93,6 +93,9 @@ export default { 'characters-removed': 'Zeichen entfernt', }, + // rule-alias-suggester.ts + 'all-rules-option': 'Alle', + // settings.ts 'linter-title': 'Linter', 'empty-search-results-text': 'Keine Einstellungen stimmen mit der Suche überein', diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 529f52ab..208c1b68 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -93,6 +93,9 @@ export default { 'characters-removed': 'characters removed', }, + // rule-alias-suggester.ts + 'all-rules-option': 'All', + // settings.ts 'linter-title': 'Linter', 'empty-search-results-text': 'No settings match search', diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts index 83a6375e..ed842c38 100644 --- a/src/lang/locale/es.ts +++ b/src/lang/locale/es.ts @@ -73,6 +73,7 @@ export default { 'characters-added': 'Caracteres añadidos', 'characters-removed': 'Caracteres eliminados', }, + 'all-rules-option': 'Todo', 'linter-title': 'Linter', 'empty-search-results-text': 'No hay configuración que coincida con la búsqueda', 'warning-text': 'Advertencia', diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index e6ac917b..8eac1875 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -93,6 +93,9 @@ export default { 'characters-removed': '字符已移除', }, + // rule-alias-suggester.ts + 'all-rules-option': '全部', + // settings.ts 'linter-title': 'Linter', 'empty-search-results-text': '没有匹配的设置项', diff --git a/src/main.ts b/src/main.ts index cdf2856f..e765f361 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,7 @@ import {SettingTab} from './ui/settings'; import {NormalArrayFormats} from './utils/yaml'; import {urlRegex} from './utils/regex'; import {getTextInLanguage, LanguageStringKey, setLanguage} from './lang/helpers'; +import {RuleAliasSuggest} from './cm6/rule-alias-suggester'; // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43 const langToMomentLocale = { @@ -89,6 +90,8 @@ export default class LinterPlugin extends Plugin { this.registerEventsAndSaveCallback(); + this.registerEditorSuggest(new RuleAliasSuggest(this)); + this.settingsTab = new SettingTab(this.app, this); this.addSettingTab(this.settingsTab); } diff --git a/src/rules.ts b/src/rules.ts index 8056b2a8..1770b4bf 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -1,13 +1,12 @@ import { - getYamlSectionValue, - loadYAML, + getExactDisabledRuleValue, + getYAMLText, QuoteCharacter, } from './utils/yaml'; import { Option, BooleanOption, } from './option'; -import {yamlRegex} from './utils/regex'; import {YAMLException} from 'js-yaml'; import {LinterError} from './linter-error'; import { @@ -166,31 +165,12 @@ export const RuleTypeOrder = Object.values(RuleType); * @return {[string[], boolean]} The list of ignored rules and whether the current file should be ignored entirely */ export function getDisabledRules(text: string): [string[], boolean] { - const yaml = text.match(yamlRegex); - if (!yaml) { + const yaml_text = getYAMLText(text); + if (yaml_text === null) { return [[], false]; } - const yaml_text = yaml[1]; - const disabledRulesValue = getYamlSectionValue(yaml_text, 'disabled rules'); - if (disabledRulesValue == null) { - return [[], false]; - } - - let disabledRulesKeyAndValue = disabledRulesValue.includes('\n') ? 'disabled rules:\n' : 'disabled rules: '; - disabledRulesKeyAndValue += disabledRulesValue; - - const parsed_yaml = loadYAML(disabledRulesKeyAndValue); - let disabled_rules = (parsed_yaml as { 'disabled rules': string[] | string })[ - 'disabled rules' - ]; - if (!disabled_rules) { - return [[], false]; - } - - if (typeof disabled_rules === 'string') { - disabled_rules = [disabled_rules]; - } + const disabled_rules = getExactDisabledRuleValue(yaml_text); if (disabled_rules.includes('all')) { return [rules.map((rule) => rule.alias), true]; diff --git a/src/rules/remove-yaml-keys.ts b/src/rules/remove-yaml-keys.ts index 88b0775d..19121afa 100644 --- a/src/rules/remove-yaml-keys.ts +++ b/src/rules/remove-yaml-keys.ts @@ -1,8 +1,7 @@ import {Options, RuleType} from '../rules'; import RuleBuilder, {ExampleBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder'; import dedent from 'ts-dedent'; -import {yamlRegex} from '../utils/regex'; -import {removeYamlSection} from '../utils/yaml'; +import {getYAMLText, removeYamlSection} from '../utils/yaml'; class RemoveYamlKeysOptions implements Options { yamlKeysToRemove: string[] = []; @@ -22,12 +21,16 @@ export default class RemoveYamlKeys extends RuleBuilder { } apply(text: string, options: RemoveYamlKeysOptions): string { const yamlKeysToRemove: string[] = options.yamlKeysToRemove; - const yaml = text.match(yamlRegex); - if (!yaml || yamlKeysToRemove.length === 0) { + if (yamlKeysToRemove.length === 0) { return text; } - let yamlText = yaml[1]; + const yaml = getYAMLText(text); + if (yaml === null) { + return text; + } + + let yamlText = yaml; for (const key of yamlKeysToRemove) { let actualKey = key.trim(); if (actualKey.endsWith(':')) { @@ -36,7 +39,7 @@ export default class RemoveYamlKeys extends RuleBuilder { yamlText = removeYamlSection(yamlText, actualKey); } - return text.replace(yaml[1], yamlText); + return text.replace(yaml, yamlText); } get exampleBuilders(): ExampleBuilder[] { return [ diff --git a/src/rules/yaml-key-sort.ts b/src/rules/yaml-key-sort.ts index 0c76c767..f222a539 100644 --- a/src/rules/yaml-key-sort.ts +++ b/src/rules/yaml-key-sort.ts @@ -1,8 +1,7 @@ import {Options, RuleType} from '../rules'; import RuleBuilder, {BooleanOptionBuilder, DropdownOptionBuilder, ExampleBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder'; import dedent from 'ts-dedent'; -import {yamlRegex} from '../utils/regex'; -import {getYamlSectionValue, loadYAML, removeYamlSection, setYamlSection} from '../utils/yaml'; +import {getYAMLText, getYamlSectionValue, loadYAML, removeYamlSection, setYamlSection} from '../utils/yaml'; type YamlSortOrderForOtherKeys = 'None' | 'Ascending Alphabetical' | 'Descending Alphabetical'; @@ -36,14 +35,12 @@ export default class YamlKeySort extends RuleBuilder { return YamlKeySortOptions; } apply(text: string, options: YamlKeySortOptions): string { - const yaml = text.match(yamlRegex); - if (!yaml) { + const oldYaml = getYAMLText(text); + if (oldYaml === null) { return text; } - const oldYaml = yaml[1]; let yamlText = oldYaml; - const priorityAtStartOfYaml: boolean = options.priorityKeysAtStartOfYaml; const yamlKeys: string[] = options.yamlKeyPrioritySortOrder; diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts index 87bf73dc..87ee44da 100644 --- a/src/utils/yaml.ts +++ b/src/utils/yaml.ts @@ -11,6 +11,7 @@ export const OBSIDIAN_ALIAS_KEY_SINGULAR = 'alias'; export const OBSIDIAN_ALIAS_KEY_PLURAL = 'aliases'; export const OBSIDIAN_ALIASES_KEYS = [OBSIDIAN_ALIAS_KEY_SINGULAR, OBSIDIAN_ALIAS_KEY_PLURAL]; export const LINTER_ALIASES_HELPER_KEY = 'linter-yaml-title-alias'; +export const DISABLED_RULES_KEY = 'disabled rules'; /** * Adds an empty YAML block to the text if it doesn't already have one. @@ -24,6 +25,15 @@ export function initYAML(text: string): string { return text; } +export function getYAMLText(text: string): string | null { + const yaml = text.match(yamlRegex); + if (!yaml) { + return null; + } + + return yaml[1]; +} + export function formatYAML(text: string, func: (text: string) => string): string { if (!text.match(yamlRegex)) { return text; @@ -407,3 +417,27 @@ function basicEscapeString(value: string, defaultEscapeCharacter: QuoteCharacter // the line must have a colon with a space return `${defaultEscapeCharacter}${value}${defaultEscapeCharacter}`; } + +export function getExactDisabledRuleValue(yaml_text: string): string[] { + const disabledRulesValue = getYamlSectionValue(yaml_text, DISABLED_RULES_KEY); + if (disabledRulesValue == null) { + return []; + } + + let disabledRulesKeyAndValue = disabledRulesValue.includes('\n') ? `${DISABLED_RULES_KEY}:\n` : `${DISABLED_RULES_KEY}: `; + disabledRulesKeyAndValue += disabledRulesValue; + + const parsed_yaml = loadYAML(disabledRulesKeyAndValue); + let disabled_rules = (parsed_yaml as { 'disabled rules': string[] | string })[ + 'disabled rules' + ]; + if (!disabled_rules) { + return []; + } + + if (typeof disabled_rules === 'string') { + disabled_rules = [disabled_rules]; + } + + return disabled_rules; +}