Skip to content

Commit

Permalink
update search_multi: add qualifier: dir
Browse files Browse the repository at this point in the history
  • Loading branch information
obgnail committed Jan 14, 2025
1 parent 47ce444 commit 61fe2c4
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 75 deletions.
13 changes: 7 additions & 6 deletions plugin/global/core/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class IPlugin {

/** 一级插件 */
class BasePlugin extends IPlugin {
call(type, meta) {}
call(action, meta) {}
}

/** 二级插件 */
Expand Down Expand Up @@ -77,8 +77,8 @@ const loadPlugin = async (fixedName, setting, isCustom) => {
}

const LoadPlugins = async (settings, isCustom) => {
const plugins = { enable: {}, disable: {}, stop: {}, error: {}, nosetting: {} };
await Promise.all(Object.entries(settings).map(async ([fixedName, setting]) => {
const plugins = { enable: {}, disable: {}, stop: {}, error: {}, nosetting: {} }
const promises = Object.entries(settings).map(async ([fixedName, setting]) => {
if (!setting) {
plugins.nosetting[fixedName] = fixedName;
} else if (!setting.ENABLE && !setting.enable) {
Expand All @@ -96,12 +96,13 @@ const LoadPlugins = async (settings, isCustom) => {
plugins.error[fixedName] = error;
}
}
}))
})
await Promise.all(promises)

// log
const LOG_COLOR = { enable: "32", disable: "33", stop: "34", error: "31", nosetting: "35" };
const COLORS = { enable: "32", disable: "33", stop: "34", error: "31", nosetting: "35" };
console.group(`${isCustom ? "Custom" : "Base"} Plugin`);
Object.entries(plugins).forEach(([t, p]) => console.debug(`[ \x1B[${LOG_COLOR[t]}m${t}\x1b[0m ] [ ${Object.keys(p).length} ]:`, p));
Object.entries(plugins).forEach(([t, p]) => console.debug(`[ \x1B[${COLORS[t]}m${t}\x1b[0m ] [ ${Object.keys(p).length} ]:`, p));
console.groupEnd();

return plugins;
Expand Down
3 changes: 3 additions & 0 deletions plugin/global/core/utils/mixin/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ class dialog {
case "span":
label = "span";
break
case "blockquote":
label = "blockquote"
break
}
const class_ = comp.inline ? "form-inline-group" : "form-block-group";
const label_ = comp.label ? `<${label}>${comp.label}${genInfo(comp)}</${label}>` : "";
Expand Down
74 changes: 33 additions & 41 deletions plugin/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,32 @@ class preferencesPlugin extends BasePlugin {
hotkey = () => [{ hotkey: this.config.HOTKEY, callback: this.call }]

getSettings = async () => {
const settings = await this.utils.runtime.readBasePluginSetting();
const customSettings = await this.utils.runtime.readCustomPluginSetting();
delete settings.global;
return [settings, customSettings]
const base = await this.utils.runtime.readBasePluginSetting()
const custom = await this.utils.runtime.readCustomPluginSetting()
delete base.global
return [base, custom]
}

togglePlugin = async (enablePlugins, enableCustomPlugins, showModal = false) => {
const [settings, customSettings] = await this.getSettings()

const pluginState = {}
const customPluginState = {}
Object.keys(settings).forEach(fixedName => (pluginState[fixedName] = { ENABLE: enablePlugins.includes(fixedName) }))
Object.keys(customSettings).forEach(fixedName => (customPluginState[fixedName] = { enable: enableCustomPlugins.includes(fixedName) }))

// check need update file
const settingsHasUpdate = Object.entries(settings).some(([name, plugin]) => plugin.ENABLE !== pluginState[name].ENABLE)
const customSettingsHasUpdate = Object.entries(customSettings).some(([name, plugin]) => plugin.enable !== customPluginState[name].enable)
if (!settingsHasUpdate && !customSettingsHasUpdate) return

const files = [
{ file: "settings.user.toml", mergeObj: pluginState },
{ file: "custom_plugin.user.toml", mergeObj: customPluginState },
]
for (const { file, mergeObj } of files) {
const settingPath = await this.utils.runtime.getActualSettingPath(file)
const settingObj = await this.utils.readTomlFile(settingPath)
const setting = this.utils.merge(settingObj, mergeObj)
const newContent = this.utils.stringifyToml(setting)
const ok = await this.utils.writeFile(settingPath, newContent)
if (!ok) return
togglePlugin = async (enableBasePlugins, enableCustomPlugins) => {
const updateSetting = async (file, setting, enablePlugins, enableKey) => {
const newState = Object.keys(setting).reduce((acc, fixedName) => {
acc[fixedName] = { [enableKey]: enablePlugins.includes(fixedName) }
return acc
}, {})
const needUpdate = Object.entries(setting).some(([name, plugin]) => plugin[enableKey] !== newState[name][enableKey])
if (needUpdate) {
const settingPath = await this.utils.runtime.getActualSettingPath(file)
const settingObj = await this.utils.readTomlFile(settingPath)
const mergedSetting = this.utils.merge(settingObj, newState)
const newContent = this.utils.stringifyToml(mergedSetting)
return this.utils.writeFile(settingPath, newContent)
}
}

if (showModal) {
const [base, custom] = await this.getSettings()
const baseUpdated = await updateSetting("settings.user.toml", base, enableBasePlugins, "ENABLE")
const customUpdated = await updateSetting("custom_plugin.user.toml", custom, enableCustomPlugins, "enable")
if (baseUpdated || customUpdated) {
const option = { type: "info", buttons: ["确定", "取消"], title: "preferences", detail: "配置将于重启 Typora 后生效,确认重启?", message: "设置成功" }
const { response } = await this.utils.showMessageBox(option)
if (response === 0) {
Expand All @@ -52,35 +45,34 @@ class preferencesPlugin extends BasePlugin {
right_click_menu: "此插件是普通用户调用其他插件的入口",
custom: "所有的二级插件都挂载在此插件上,停用会导致所有的二级插件失效",
json_rpc: "此插件面向开发者",
ripgrep: "此插件需要您了解 ripgrep 工具",
test: "此插件面向开发者,建议仅在开发插件期间启用",
reopenClosedFiles: "此插件依赖「标签页管理」插件",
redirectLocalRootUrl: "此插件手动修改配置后才可运行",
article_uploader: "此插件面向特殊人群,手动修改配置后才可运行",
}
const displayFunc = ([fixedName, plugin]) => ({
const display = ([fixedName, plugin]) => ({
label: `${plugin.NAME || plugin.name}${fixedName})`,
info: INFO[fixedName],
value: fixedName,
checked: plugin.ENABLE || plugin.enable,
disabled: this.config.IGNORE_PLUGINS.includes(fixedName),
})
const onclick = ev => ev.target.closest("a") && this.utils.runtime.openSettingFolder();
const [settings, customSettings] = await this.getSettings();
const plugins = Object.entries(settings).map(displayFunc);
const customPlugins = Object.entries(customSettings).map(displayFunc);
const [base, custom] = await this.getSettings()
const basePlugins = Object.entries(base).map(display)
const customPlugins = Object.entries(custom).map(display)
const onclick = ev => ev.target.closest("a") && this.utils.runtime.openSettingFolder()
const components = [
{ label: "为了保护用户,此处禁止启停部分插件,如需请 <a>修改配置文件</a>", type: "p", onclick },
{ label: "", legend: "一级插件", type: "checkbox", list: plugins },
{ label: "", legend: "一级插件", type: "checkbox", list: basePlugins },
{ label: "", legend: "二级插件", type: "checkbox", list: customPlugins },
];
const { response, submit: [_, p1, p2] } = await this.utils.dialog.modalAsync({ title: "启停插件", width: "450px", components });
]
const modal = { title: "启停插件", width: "450px", components }
const { response, submit: [_, _base, _custom] } = await this.utils.dialog.modalAsync(modal)
if (response === 1) {
await this.togglePlugin(p1, p2, true);
await this.togglePlugin(_base, _custom)
}
}
}

module.exports = {
plugin: preferencesPlugin
};
}
63 changes: 35 additions & 28 deletions plugin/search_multi.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class searchMultiKeywordPlugin extends BasePlugin {
hotkey = () => [{ hotkey: this.config.HOTKEY, callback: this.call }]

init = () => {
this.searchHelper = new SearchHelper(this)
this.highlightHelper = new Highlighter(this)
this.searcher = new Searcher(this)
this.highlighter = new Highlighter(this)
this.allowedExtensions = new Set(this.config.ALLOW_EXT.map(ext => ext.toLowerCase()))
this.entities = {
modal: document.querySelector("#plugin-search-multi"),
Expand All @@ -55,8 +55,8 @@ class searchMultiKeywordPlugin extends BasePlugin {
}

process = () => {
this.searchHelper.process()
this.highlightHelper.process()
this.searcher.process()
this.highlighter.process()
if (this.config.ALLOW_DRAG) {
this.utils.dragFixedModal(this.entities.input, this.entities.modal)
}
Expand All @@ -72,7 +72,7 @@ class searchMultiKeywordPlugin extends BasePlugin {
if (!btn) return
const action = btn.getAttribute("action")
if (action === "searchGrammarModal") {
this.searchHelper.showGrammar()
this.searcher.showGrammar()
} else if (action === "toggleCaseSensitive") {
btn.classList.toggle("select")
this.config.CASE_SENSITIVE = !this.config.CASE_SENSITIVE
Expand Down Expand Up @@ -122,8 +122,8 @@ class searchMultiKeywordPlugin extends BasePlugin {
if (!input) return

try {
const ast = this.searchHelper.parse(input)
const explain = this.searchHelper.toExplain(ast)
const ast = this.searcher.parse(input)
const explain = this.searcher.toExplain(ast)
this.entities.input.setAttribute("title", explain)
this.utils.notification.hide()
return ast
Expand All @@ -139,10 +139,10 @@ class searchMultiKeywordPlugin extends BasePlugin {
ast = ast || this.getAST()
this.utils.hide(this.entities.highlightResult)
if (!ast) return
const tokens = this.searchHelper.getContentTokens(ast).filter(Boolean)
const tokens = this.searcher.getContentTokens(ast).filter(Boolean)
if (!tokens || tokens.length === 0) return

const hitGroups = this.highlightHelper.doSearch(tokens)
const hitGroups = this.highlighter.doSearch(tokens)
const itemList = Object.entries(hitGroups).map(([cls, { name, hits }]) => {
const div = document.createElement("div")
div.className = `plugin-highlight-multi-result-item ${cls}`
Expand All @@ -161,7 +161,7 @@ class searchMultiKeywordPlugin extends BasePlugin {

searchMultiByAST = async (rootPath, ast) => {
const { fileFilter, dirFilter } = this._getFilter()
const matcher = source => this.searchHelper.match(ast, source)
const matcher = source => this.searcher.match(ast, source)
const callback = this._showResultItem(rootPath, matcher)
await this._traverseDir(rootPath, fileFilter, dirFilter, callback)
}
Expand Down Expand Up @@ -246,7 +246,7 @@ class searchMultiKeywordPlugin extends BasePlugin {
hide = () => {
this.utils.hide(this.entities.modal)
this.utils.hide(this.entities.info)
this.highlightHelper.clearSearch()
this.highlighter.clearSearch()
}

show = () => {
Expand Down Expand Up @@ -376,7 +376,7 @@ class QualifierMixin {
* 4. query: Queries the file data to obtain `queryResult`.
* 5. match: Matches `castResult` from step 3 with `queryResult` from step 4.
*/
class SearchHelper {
class Searcher {
constructor(plugin) {
this.MIXIN = QualifierMixin
this.config = plugin.config
Expand Down Expand Up @@ -414,6 +414,7 @@ class SearchHelper {
default: ({ path, file, stats, buffer }) => `${buffer.toString()}\n${path}`,
path: ({ path, file, stats, buffer }) => path,
file: ({ path, file, stats, buffer }) => file,
dir: ({ path, file, stats, buffer }) => this.utils.Package.Path.dirname(path),
ext: ({ path, file, stats, buffer }) => this.utils.Package.Path.extname(file),
content: ({ path, file, stats, buffer }) => buffer.toString(),
time: ({ path, file, stats, buffer }) => this.MIXIN.QUERY.toDate(stats.mtime),
Expand All @@ -440,6 +441,7 @@ class SearchHelper {
{ scope: "default", name: "内容或路径", is_meta: false, query: QUERY.default },
{ scope: "path", name: "路径", is_meta: true, query: QUERY.path },
{ scope: "file", name: "文件名", is_meta: true, query: QUERY.file },
{ scope: "dir", name: "所属目录", is_meta: true, query: QUERY.dir },
{ scope: "ext", name: "扩展名", is_meta: true, query: QUERY.ext },
{ scope: "content", name: "内容", is_meta: false, query: QUERY.content },
{ scope: "frontmatter", name: "FrontMatter", is_meta: false, query: QUERY.frontmatter },
Expand Down Expand Up @@ -811,8 +813,8 @@ class SearchHelper {
const genOperator = (...operators) => operators.map(operator => `<code>${operator}</code>`).join("、")
const genUL = (...li) => `<ul style="padding-left: 1em; word-break: break-word;">${li.map(e => `<li>${e}</li>`).join("")}</ul>`
const scopeDesc = genUL(
`文件属性${genScope(metaScope)}`,
`内容属性${genScope(contentScope)}`,
`元数据搜索符${genScope(metaScope)}`,
`内容搜索符${genScope(contentScope)}`,
`默认值 default = path + content(路径+文件内容)`,
)
const operatorDesc = genUL(
Expand All @@ -823,18 +825,18 @@ class SearchHelper {

const genInfo = title => `<span class="modal-label-info ion-information-circled" title="${title}"></span>`
const scopeInfo = genInfo('具体来说:文件路径或文件内容包含 pear')
const diffInfo = genInfo('注意区分:\n「head=plugin表示标题为plugin,当标题为”typora plugin“时不可匹配\n「head:plugin表示标题包含plugin,当标题为”typora plugin“时可以匹配')
const diffInfo = genInfo('注意区分:\nhead=plugin 表示标题为plugin,当标题为”typora plugin“时不可匹配\nhead:plugin 表示标题包含plugin,当标题为”typora plugin“时可以匹配')

const keywordDesc = `
<table>
<tr><th>关键字</th><th>说明</th></tr>
<tr><td>空格</td><td>表示与。文档应该同时满足空格左右两侧的查询条件,等价于 AND</td></tr>
<tr><td>|</td><td>表示或。文档应该满足 | 左右两侧中至少一个查询条件,等价于 OR</td></tr>
<tr><td>-</td><td>表示非。文档不可满足 - 右侧的查询条件</td></tr>
<tr><td>""</td><td>表示词组。引号包裹视为词组</td></tr>
<tr><td>/RegExp/</td><td>JavaScript 风格的正则表达式</td></tr>
<tr><td>scope</td><td>查询属性,用于限定查询条件${scopeDesc}</td></tr>
<tr><td>operator</td><td>操作符,用于比较查询条件和查询结果${operatorDesc}</td></tr>
<tr><td>空格</td><td>连接两个查询条件,表示逻辑与。文档应该同时满足空格左右两侧的查询条件,等价于 AND</td></tr>
<tr><td>|</td><td>连接两个查询条件,表示逻辑或。文档应该满足 | 左右两侧中至少一个查询条件,等价于 OR</td></tr>
<tr><td>-</td><td>后接一个查询条件,表示逻辑非。文档不可满足 - 右侧的查询条件</td></tr>
<tr><td>""</td><td>引号包裹文本,表示词组。</td></tr>
<tr><td>/regex/</td><td>JavaScript 风格的正则表达式</td></tr>
<tr><td>scope</td><td>搜索符,用于限定查询条件${scopeDesc}</td></tr>
<tr><td>operator</td><td>操作符,用于比较查询关键字和查询结果${operatorDesc}</td></tr>
<tr><td>()</td><td>小括号,用于调整运算优先级</td></tr>
</table>`

Expand All @@ -848,9 +850,9 @@ class SearchHelper {
<tr><td>sour pear -apple</td><td>包含 sour 和 pear,且不含 apple</td></tr>
<tr><td>/\\bsour\\b/ pear time=2024-03-12</td><td>匹配正则\\bsour\\b(全字匹配sour),且包含 pear,且文件更新时间为 2024-03-12</td></tr>
<tr><td>frontmatter:开发 | head=plugin | strong:MIT</td><td>YAML Front Matter 包含开发 或者 标题内容为 plugin 或者 加粗文字包含 MIT ${diffInfo}</td></tr>
<tr><td>size>10k (linenum>=1000 | hasimage=true)</td><td>文件大小超过 10KB,并且文件要么至少有 1000 行,要么包含图片</td></tr>
<tr><td>size>10kb (linenum>=1000 | hasimage=true)</td><td>文件大小超过 10KB,并且文件要么至少有 1000 行,要么包含图片</td></tr>
<tr><td>path:(info | warn | err) -ext:md</td><td>文件路径包含 info 或 warn 或 err,且扩展名不含 md</td></tr>
<tr><td>file:/[a-z]{3}/ content:prometheus blockcode:"kubectl apply"</td><td>文件名匹配正则 [a-z]{3},且内容包含 prometheus,且代码块内容含有 kubectl apply</td></tr>
<tr><td>file:/[a-z]{3}/ content:prometheus blockcode:"kubectl apply"</td><td>文件名匹配正则 [a-z]{3}(包含三个小写字母),且文件内容包含 prometheus,且代码块内容包含 kubectl apply</td></tr>
</table>`

const content = `
Expand All @@ -859,20 +861,25 @@ class SearchHelper {
<term> ::= <factor> ( <conjunction> <factor> )*
<factor> ::= <qualifier>? <match>
<qualifier> ::= <scope> <operator>
<match> ::= <keyword> | '"'<keyword>'"' | '/'<regexp>'/' | '('<expression>')'
<match> ::= <keyword> | '"'<keyword>'"' | '/'<regex>'/' | '('<expression>')'
<conjunction> ::= <and> | <not>
<or> ::= 'OR' | '|'
<and> ::= 'AND' | ' '
<not> ::= '-'
<keyword> ::= [^\\s"()|]+
<regexp> ::= [^/]+
<regex> ::= [^/]+
<operator> ::= ${operator.map(s => `'${s}'`).join(" | ")}
<scope> ::= ${[...metaScope, ...contentScope].map(s => `'${s.scope}'`).join(" | ")}`

const desc = `高级搜索通过组合不同的条件来精确查找文件。每个条件由三部分组成:搜索符、操作符、关键字,例如 size>2kb(含义:文件尺寸大于 2KB)、ext:txt(含义:文件扩展名包含 txt)。
条件之间用空格分隔,表示所有条件都必须满足,例如 size>2kb ext:txt;如果只需满足其一条件,请使用 OR 连接,例如 size>2kb OR ext:txt;如果需满足前者,并且排除后者,请使用 - 连接,例如 size>2kb -ext:txt`
const components = [
{ label: keywordDesc, type: "p" },
{ label: desc, type: "blockquote" },
{ label: example, type: "p" },
{ label: "", type: "textarea", rows: 20, content, title: "这段文字是语法的形式化表述,你可以把它塞给AI,AI会为你解释" },
{ label: "具体用法", type: "p" },
{ label: keywordDesc, type: "p" },
{ label: "形式文法", type: "p" },
{ label: "", type: "textarea", rows: 20, content },
]
this.utils.dialog.modal({ title: "高级搜索", width: "600px", components })
}
Expand Down

0 comments on commit 61fe2c4

Please sign in to comment.