├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── LICENSE
├── README-zh_CN.md
├── README.md
├── esbuild.config.mjs
├── manifest.json
├── package.json
├── screenshot
└── demo.gif
├── src
├── SearchBox.svelte
├── editor-extension.ts
├── i18n
│ ├── index.ts
│ └── locale
│ │ ├── en.json
│ │ └── zh_cn.json
├── main.ts
├── styles
│ └── index.scss
├── types
│ └── global.d.ts
└── util
│ ├── common-helper.ts
│ └── text-helper.ts
├── style-settings.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 | dist
11 | package-lock.json
12 |
13 | # Don't include the compiled main.js file in the repo.
14 | # They should be uploaded to GitHub releases instead.
15 | main.js
16 |
17 | # Exclude sourcemaps
18 | *.map
19 |
20 | # obsidian
21 | data.json
22 |
23 | # Exclude macOS Finder (System Explorer) View States
24 | .DS_Store
25 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 nyable
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-zh_CN.md:
--------------------------------------------------------------------------------
1 | # Obsidian 文本查找器
2 |
3 | [English](README.md)
4 | [中文说明](README-zh_CN.md)
5 |
6 | 这是一个用于 Obsidian 的插件(https://obsidian.md)。
7 | 在编辑模式下提供类似于 VSCode 的搜索/替换窗口。
8 |
9 | **注意:** Obsidian API 仍处于早期 alpha 阶段,可能随时发生变化!
10 |
11 | ## 功能
12 |
13 | 在 **编辑模式** 下搜索或替换当前 Markdown 文件的文本。
14 |
15 | - 在当前文件中搜索/替换
16 | - 高亮匹配文本
17 | - 显示匹配数量
18 | - 支持正则表达式
19 | - 支持区分大小写
20 |
21 | ## To Do
22 |
23 | - [ ] 在选取范围内搜索/替换
24 | - [ ] 输入历史
25 |
26 | ## 截图
27 |
28 | 
29 |
30 | # 如何使用
31 |
32 | ## 安装
33 |
34 | ### 社区插件
35 |
36 | - 在社区插件市场中搜索"text finder"并安装。
37 | - 在 Obsidian 设置中启用插件。
38 |
39 | ### 使用 BRAT 安装
40 |
41 | - [安装 BRAT 插件](https://obsidian.md/plugins?id=obsidian42-brat)。
42 | - 执行命令 `Obsidian42 - BRAT: Add a beta plugin for testing`。
43 | - 粘贴此存储库的 URL 并确认。
44 | - 在 Obsidian 设置中启用插件。
45 |
46 | ### 从源代码安装
47 |
48 | - 克隆此仓库。
49 | - 使用 `npm i` 或 `yarn` 安装依赖。
50 | - 使用 `npm run build` 在 `./dist` 中构建文件。
51 | - 将 `main.js`、`styles.css`、`manifest.json` 复制到你的 vault 的 `VaultFolder/.obsidian/plugins/text-finder/` 中。
52 | - 在 Obsidian 设置中启用插件。
53 |
54 | ### 从 Release 安装
55 |
56 | - 在 [最新发布版本](https://github.com/nyable/obsidian-text-finder/releases/latest) 中下载 `main.js`、`styles.css`、`manifest.json`。
57 | - 将 `main.js`、`styles.css`、`manifest.json` 复制到你的 vault 的 `VaultFolder/.obsidian/plugins/text-finder/` 中。
58 | - 在 Obsidian 设置中启用插件。
59 |
60 | ## 设置
61 |
62 | - 在设置页面为对应的命令设置快捷键。
63 |
64 | ## 自定义样式
65 |
66 | 使用 [CSS 代码片段](https://help.obsidian.md/Extending+Obsidian/CSS+snippets)或[Style Settings Plugin](https://github.com/mgmeyers/obsidian-style-settings)来自定义样式。
67 |
68 | [示例的 Style Settings 配置文件](./style-settings.css).
69 |
70 | 以下是部分 CSS 片段示例。
71 |
72 | ### 匹配高亮与当前项
73 |
74 | ```css
75 | .nya-text-finder-match {
76 | border-radius: 2px;
77 | box-shadow: 0 0 0 1px rgb(60, 115, 75);
78 | background-color: inherit;
79 | color: inherit;
80 | }
81 |
82 | .nya-text-finder-match-current {
83 | box-shadow: 0 0 0 1px rgb(187, 187, 187);
84 | background-color: rgba(255, 170, 0, 0.8);
85 | color: black;
86 | }
87 | ```
88 |
89 | ### 修改位置
90 |
91 | 左上角
92 |
93 | ```css
94 | .nya-finder {
95 | right: unset !important;
96 | left: 376px;
97 | }
98 | ```
99 |
100 | 右下角
101 |
102 | ```css
103 | .nya-finder {
104 | top: unset !important;
105 | bottom: 72px;
106 | }
107 | ```
108 |
109 | 左下角
110 |
111 | ```css
112 | .nya-finder {
113 | top: unset !important;
114 | right: unset !important;
115 | left: 376px;
116 | bottom: 72px;
117 | }
118 | ```
119 |
120 | ### 更改聚焦时的边框颜色
121 |
122 | ```css
123 | body {
124 | --nya-focus-border-color: #39c5bb;
125 | }
126 | ```
127 |
128 | ### 更改折叠按钮的颜色
129 |
130 | ```css
131 | .nya-toggle {
132 | border-left: 3px solid red !important;
133 | }
134 | ```
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Text Finder
2 |
3 | [English](README.md)
4 | [中文说明](README-zh_CN.md)
5 |
6 | This plugin for Obsidian(https://obsidian.md).
7 | Provides a search/replace window similar to VSCode in editing mode.
8 |
9 | **Note:** The Obsidian API is still in early alpha and is subject to change at any time!
10 |
11 | ## Feature
12 |
13 | Search or replace the text of the current MarkDown file **in editor mode**.
14 |
15 | - Search/Replace in current file
16 | - Highlight matching text
17 | - Show number of matches
18 | - Supports regular expressions
19 | - Supports case sensitivity
20 |
21 | ## To Do
22 |
23 | - [ ] Search/Replace in Selection
24 | - [ ] Input history
25 |
26 | ## ScreenShot
27 |
28 | 
29 |
30 | # How to use
31 |
32 | ## Install
33 |
34 | ### Community plugins
35 |
36 | - Search for "text finder" in the community plugins and install it.
37 | - Enable plugin in Obsidian setting.
38 |
39 | ### BRAT
40 |
41 | - [Install the BRAT Plugin](https://obsidian.md/plugins?id=obsidian42-brat)
42 | - Execute command `Obsidian42 - BRAT: Add a beta plugin for testing`
43 | - Paste the URL of this repository and confirm
44 | - Enable plugin in Obsidian setting.
45 |
46 | ### Source Code
47 |
48 | - Clone this repo.
49 | - `npm i` or `yarn` to install dependencies
50 | - `npm run build` to build file in `./dist`.
51 | - Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/text-finder/`.
52 | - Enable plugin in Obsidian setting.
53 |
54 | ### Releases
55 |
56 | - Download `main.js`, `styles.css`, `manifest.json` in the [latest release](https://github.com/nyable/obsidian-text-finder/releases/latest)
57 | - Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/text-finder/`.
58 | - Enable plugin in Obsidian setting.
59 |
60 | ## Settings
61 |
62 | - Assign a hotkeys for the plugin's command.
63 |
64 | ## Customize Style
65 |
66 | Use [CSS snippets](https://help.obsidian.md/Extending+Obsidian/CSS+snippets) or [Style Settings Plugin](https://github.com/mgmeyers/obsidian-style-settings) to customize styles.
67 |
68 | [Example Style Settings configuration file](./style-settings.css).
69 |
70 | Here are some CSS snippets examples.
71 |
72 | ### Match Highlight and Current Item
73 |
74 | ```css
75 | .nya-text-finder-match {
76 | border-radius: 2px;
77 | box-shadow: 0 0 0 1px rgb(60, 115, 75);
78 | background-color: inherit;
79 | color: inherit;
80 | }
81 |
82 | .nya-text-finder-match-current {
83 | box-shadow: 0 0 0 1px rgb(187, 187, 187);
84 | background-color: rgba(255, 170, 0, 0.8);
85 | color: black;
86 | }
87 | ```
88 |
89 | ### Change Position
90 |
91 | Top Left
92 |
93 | ```css
94 | .nya-finder {
95 | right: unset !important;
96 | left: 376px;
97 | }
98 | ```
99 |
100 | Bottom Right
101 |
102 | ```css
103 | .nya-finder {
104 | top: unset !important;
105 | bottom: 72px;
106 | }
107 | ```
108 |
109 | Bottom Left
110 |
111 | ```css
112 | .nya-finder {
113 | top: unset !important;
114 | right: unset !important;
115 | left: 376px;
116 | bottom: 72px;
117 | }
118 | ```
119 |
120 | ### Change the border color when focused
121 |
122 | ```css
123 | body {
124 | --nya-focus-border-color: #39c5bb;
125 | }
126 | ```
127 |
128 | ### Change the color of the collapse button
129 |
130 | ```css
131 | .nya-toggle {
132 | border-left: 3px solid red !important;
133 | }
134 | ```
135 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 | import esbuildSvelte from "esbuild-svelte";
5 | import { sveltePreprocess } from "svelte-preprocess";
6 | import { sassPlugin } from "esbuild-sass-plugin";
7 |
8 | const banner = `/*
9 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
10 | if you want to view the source, please visit the github repository of this plugin
11 | */
12 | `;
13 |
14 | const prod = process.argv[2] === "production";
15 |
16 | const context = await esbuild.context({
17 | plugins: [
18 | esbuildSvelte({
19 | compilerOptions: { css: "injected" },
20 | preprocess: sveltePreprocess(),
21 | }),
22 | sassPlugin(),
23 | ],
24 | banner: {
25 | js: banner,
26 | },
27 | entryPoints: [
28 | "src/main.ts",
29 | { in: "src/styles/index.scss", out: "styles" },
30 | ],
31 | bundle: true,
32 | external: [
33 | "obsidian",
34 | "electron",
35 | "@codemirror/autocomplete",
36 | "@codemirror/collab",
37 | "@codemirror/commands",
38 | "@codemirror/language",
39 | "@codemirror/lint",
40 | "@codemirror/search",
41 | "@codemirror/state",
42 | "@codemirror/view",
43 | "@lezer/common",
44 | "@lezer/highlight",
45 | "@lezer/lr",
46 | ...builtins,
47 | ],
48 | format: "cjs",
49 | target: "es2018",
50 | logLevel: "info",
51 | sourcemap: prod ? false : "inline",
52 | treeShaking: true,
53 | outdir: "dist",
54 | minify: prod,
55 | });
56 |
57 | if (prod) {
58 | await context.rebuild();
59 | process.exit(0);
60 | } else {
61 | await context.watch();
62 | }
63 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "text-finder",
3 | "name": "Text Finder",
4 | "version": "0.2.2",
5 | "minAppVersion": "0.15.0",
6 | "description": "Provides a find/replace window in edit mode similar to VSCode (supports regular expressions and case sensitivity).",
7 | "author": "hafuhafu",
8 | "authorUrl": "https://github.com/nyable",
9 | "fundingUrl": "https://buymeacoffee.com/nyable",
10 | "isDesktopOnly": false
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-text-finder",
3 | "version": "0.2.2",
4 | "description": "Provides a find/replace window in edit mode similar to VSCode (supports regular expressions and case sensitivity).",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [
12 | "obsidian",
13 | "text",
14 | "find",
15 | "replace",
16 | "regular express"
17 | ],
18 | "author": "hafuhafu",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "@tsconfig/svelte": "5.0.4",
22 | "@types/node": "^16.11.6",
23 | "@typescript-eslint/eslint-plugin": "5.29.0",
24 | "@typescript-eslint/parser": "5.29.0",
25 | "builtin-modules": "3.3.0",
26 | "esbuild": "0.25.0",
27 | "esbuild-sass-plugin": "^3.3.1",
28 | "esbuild-svelte": "0.8.2",
29 | "obsidian": "latest",
30 | "postcss": "8.4.47",
31 | "sass": "^1.79.4",
32 | "svelte": "4.2.19",
33 | "svelte-preprocess": "6.0.3",
34 | "tslib": "2.4.0",
35 | "typescript": "5.6.2"
36 | },
37 | "dependencies": {
38 | "@codemirror/language": "6.10.3",
39 | "@codemirror/state": "^6.0.0",
40 | "@codemirror/view": "^6.23.0",
41 | "i18next": "23.15.1",
42 | "lucide-svelte": "^0.446.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/screenshot/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nyable/obsidian-text-finder/53509ecc3ec3f4ee7f081055395fc18834eb2272/screenshot/demo.gif
--------------------------------------------------------------------------------
/src/SearchBox.svelte:
--------------------------------------------------------------------------------
1 |
428 |
429 |
435 |
436 |
437 |
445 | {#if isCollapsed}
446 |
447 | {:else}
448 |
449 | {/if}
450 |
451 |
452 |
453 |
467 |
468 | {#if isMobile}
469 |
470 |
471 | {#if cache.matches.length > 0}
472 |
473 | {i18n.t("search.tip.HasResults", {
474 | current: cache.index + 1,
475 | total: cache.matches.length,
476 | })}
477 |
478 | {:else}
479 |
480 | {i18n.t("search.tip.NoResults")}
481 |
482 | {/if}
483 |
484 |
492 |
493 |
494 |
495 | {:else}
496 |
497 | {#if cache.matches.length > 0}
498 |
499 | {i18n.t("search.tip.HasResults", {
500 | current: cache.index + 1,
501 | total: cache.matches.length,
502 | })}
503 |
504 | {:else}
505 |
506 | {i18n.t("search.tip.NoResults")}
507 |
508 | {/if}
509 |
510 | {/if}
511 |
512 |
521 |
522 |
523 |
532 |
533 |
534 |
544 |
554 | {#if !isMobile}
555 |
563 |
564 |
565 | {/if}
566 |
567 |
568 |
569 |
581 |
582 |
590 |
591 |
592 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
810 |
--------------------------------------------------------------------------------
/src/editor-extension.ts:
--------------------------------------------------------------------------------
1 | import type TextFinderPlugin from "./main";
2 | import {
3 | RangeSetBuilder,
4 | StateEffect,
5 | StateField,
6 | Transaction,
7 | type Extension,
8 | } from "@codemirror/state";
9 | import { Decoration, type DecorationSet, EditorView } from "@codemirror/view";
10 | import {
11 | debounce,
12 | Editor,
13 | MarkdownView,
14 | WorkspaceLeaf,
15 | type MarkdownFileInfo,
16 | } from "obsidian";
17 | import SearchBox from "./SearchBox.svelte";
18 | import { i18n } from "./i18n";
19 | import { generateUniqueId } from "./util/common-helper";
20 |
21 | export const CLS = {
22 | FINDER: "nya-finder",
23 | MATCH: "nya-text-finder-match",
24 | MATCH_CURRENT: "nya-text-finder-match-current",
25 | };
26 | export const CMD = {
27 | SHOW_FIND: "text-finder-show-find",
28 | SHOW_FIND_AND_REPLACE: "text-finder-show-find-and-replace",
29 | HIDE_FIND: "text-finder-hide-find",
30 | TOGGLE_REPLACE: "text-finder-toggle-replace",
31 | TOGGLE_FIND: "text-finder-toggle-find",
32 | PREVIOUS_MATCH: "text-finder-previous-match",
33 | NEXT_MATCH: "text-finder-next-match",
34 | REPLACE: "text-finder-replace",
35 | REPLACE_ALL: "text-finder-replace-all",
36 | };
37 |
38 | export class EditorSearch {
39 | plugin: TextFinderPlugin;
40 | // 用来缓存各个窗口的finder,适配在新窗口打开的情况;暂时没对新窗口打开后关闭进行缓存清理,不过问题应该不大。
41 | finderCache: Map = new Map();
42 |
43 | constructor(plugin: TextFinderPlugin) {
44 | this.plugin = plugin;
45 | this.registerEvent();
46 | this.registerCommand();
47 | }
48 |
49 | /**
50 | * 获取Finder组件,如果不存在则创建
51 | * @returns SearchBox
52 | */
53 | getFinder(): SearchBox {
54 | // const targetEl = this.plugin.app.workspace.containerEl;
55 | const view =
56 | this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
57 | if (!view) {
58 | throw new Error(
59 | "Get Text-Finder Error: plugin.app.workspace.getActiveViewOfType(MarkdownView) is null"
60 | );
61 | }
62 | const targetEl = view.containerEl;
63 | const finderEl = targetEl.querySelector(`.${CLS.FINDER}`);
64 | if (finderEl) {
65 | // 创建SearchBox组件的时候一定会传一个cid,因此这里可以放心取值
66 | const cid = finderEl.getAttribute("data-cid");
67 | if (cid) {
68 | const cachedFinder = this.finderCache.get(cid);
69 | if (cachedFinder) {
70 | return cachedFinder;
71 | } else {
72 | finderEl.remove();
73 | throw new Error(
74 | "Get Text-Finder Error: this.finderCache.get(cid) is null"
75 | );
76 | }
77 | } else {
78 | finderEl.remove();
79 | throw new Error(
80 | "Get Text-Finder Error: finderEl.getAttribute('data-cid') is null"
81 | );
82 | }
83 | } else {
84 | const cid = generateUniqueId();
85 | const finder = new SearchBox({
86 | target: targetEl,
87 | props: {
88 | editorSearch: this,
89 | cid: cid,
90 | },
91 | });
92 | this.registerFinderEvent(targetEl, finder);
93 | this.finderCache.set(cid, finder);
94 | return finder;
95 | }
96 | }
97 | /**
98 | * 销毁所有finder。在onunload的时候调用
99 | */
100 | destoryAll() {
101 | this.finderCache.forEach((finder) => {
102 | finder && finder.$destroy();
103 | });
104 | this.finderCache.clear();
105 | }
106 |
107 | /**
108 | * 注册finder的事件
109 | * @param mountEl 挂载的元素
110 | * @param finder finder
111 | */
112 | private registerFinderEvent(mountEl: HTMLElement, finder: SearchBox) {
113 | this.plugin.registerDomEvent(mountEl, "keydown", (e) => {
114 | // press esc
115 | if (e.key == "Escape" && finder) {
116 | finder.closeFinder();
117 | }
118 | });
119 | }
120 | private registerEvent() {
121 | const workspace = this.plugin.app.workspace;
122 |
123 | this.plugin.registerEvent(
124 | workspace.on(
125 | "active-leaf-change",
126 | debounce(
127 | (leaf: WorkspaceLeaf | null) => {
128 | if (leaf?.view instanceof MarkdownView) {
129 | this.getFinder().matchAgain();
130 | }
131 | },
132 | 250,
133 | true
134 | )
135 | )
136 | );
137 |
138 | this.plugin.registerEvent(
139 | workspace.on(
140 | "editor-change",
141 | (edt: Editor, info: MarkdownView | MarkdownFileInfo) => {
142 | this.getFinder().matchAgain(false);
143 | }
144 | )
145 | );
146 | }
147 |
148 | private openObsidianSearch() {
149 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
150 | (this.plugin.app as any).commands.commands[
151 | "editor:open-search"
152 | ].checkCallback();
153 | }
154 |
155 | private registerCommand() {
156 | const { plugin } = this;
157 | plugin.addCommand({
158 | id: CMD.SHOW_FIND,
159 | name: i18n.t("commands.ShowFind.name"),
160 | checkCallback: (checking: boolean) => {
161 | const view =
162 | plugin.app.workspace.getActiveViewOfType(MarkdownView);
163 | if (view) {
164 | const mode = view.getMode();
165 | const { useObsidianSearchInRead, useSelectionAsSearch } =
166 | plugin.settings;
167 | const previewCheck =
168 | useObsidianSearchInRead && mode === "preview";
169 | const sourceCheck = mode === "source";
170 | if (previewCheck || sourceCheck) {
171 | if (!checking) {
172 | if (mode === "preview") {
173 | this.openObsidianSearch();
174 | } else if (mode === "source") {
175 | const defaultSearchText = useSelectionAsSearch
176 | ? view.editor.getSelection()
177 | : "";
178 | this.getFinder().setVisible(
179 | true,
180 | defaultSearchText
181 | );
182 | }
183 | }
184 | return true;
185 | }
186 | }
187 | return false;
188 | },
189 | });
190 | plugin.addCommand({
191 | id: CMD.SHOW_FIND_AND_REPLACE,
192 | name: i18n.t("commands.ShowFindAndReplace.name"),
193 | editorCallback: (editor, ctx) => {
194 | const finder = this.getFinder();
195 | finder.setCollapse(false);
196 | const { useSelectionAsSearch } = plugin.settings;
197 | const defaultSearchText = useSelectionAsSearch
198 | ? editor.getSelection()
199 | : "";
200 | finder.setVisible(true, defaultSearchText);
201 | },
202 | });
203 | plugin.addCommand({
204 | id: CMD.HIDE_FIND,
205 | name: i18n.t("commands.HideFind.name"),
206 | editorCallback: (editor, ctx) => {
207 | this.getFinder().setVisible(false);
208 | },
209 | });
210 |
211 | plugin.addCommand({
212 | id: CMD.TOGGLE_FIND,
213 | name: i18n.t("commands.ToggleFind.name"),
214 | editorCallback: (editor, ctx) => {
215 | this.getFinder().toggleVisible();
216 | },
217 | });
218 |
219 | plugin.addCommand({
220 | id: CMD.TOGGLE_REPLACE,
221 | name: i18n.t("commands.ToggleReplace.name"),
222 | editorCallback: (editor, ctx) => {
223 | this.getFinder().toggleCollapse();
224 | },
225 | });
226 |
227 | plugin.addCommand({
228 | id: CMD.PREVIOUS_MATCH,
229 | name: i18n.t("commands.PreviousMatch.name"),
230 | editorCallback: (editor, ctx) => {
231 | this.getFinder().toPreviousMatch();
232 | },
233 | });
234 |
235 | plugin.addCommand({
236 | id: CMD.NEXT_MATCH,
237 | name: i18n.t("commands.NextMatch.name"),
238 | editorCallback: (editor, ctx) => {
239 | this.getFinder().toNextMatch();
240 | },
241 | });
242 | plugin.addCommand({
243 | id: CMD.REPLACE,
244 | name: i18n.t("commands.Replace.name"),
245 | editorCallback: (editor, ctx) => {
246 | const finder = this.getFinder();
247 | finder.replaceMatchedText(finder.getSearchCache().replace);
248 | },
249 | });
250 | plugin.addCommand({
251 | id: CMD.REPLACE_ALL,
252 | name: i18n.t("commands.ReplaceAll.name"),
253 | editorCallback: (editor, ctx) => {
254 | const finder = this.getFinder();
255 | finder.replaceAllMatchedText(finder.getSearchCache().replace);
256 | },
257 | });
258 | }
259 | }
260 | export const searchCacheEffect = StateEffect.define();
261 |
262 | export function editorExtensionProvider(plugin: TextFinderPlugin) {
263 | const workspace = plugin.app.workspace;
264 |
265 | workspace.onLayoutReady(() => {
266 | plugin.editorSearch = new EditorSearch(plugin);
267 |
268 | const textMatchMarker = StateField.define({
269 | create(state): DecorationSet {
270 | return Decoration.none;
271 | },
272 | update(
273 | oldState: DecorationSet,
274 | transaction: Transaction
275 | ): DecorationSet {
276 | for (const effect of transaction.effects) {
277 | if (effect.is(searchCacheEffect)) {
278 | const cache = effect.value;
279 | const builder = new RangeSetBuilder();
280 | if (cache.visible) {
281 | const length = transaction.state.doc.length;
282 | cache.matches.forEach(
283 | (item: MatchOffset, index: number) => {
284 | const from = item.from;
285 | const to = item.to;
286 | if (to <= length) {
287 | const classStr = `
288 | ${CLS.MATCH}
289 | ${cache.index == index ? CLS.MATCH_CURRENT : ""}
290 | `;
291 | builder.add(
292 | from,
293 | to,
294 | Decoration.mark({
295 | class: classStr,
296 | })
297 | );
298 | }
299 | }
300 | );
301 | }
302 | return builder.finish();
303 | }
304 | }
305 | return oldState;
306 | },
307 | provide(field: StateField): Extension {
308 | return EditorView.decorations.from(field);
309 | },
310 | });
311 |
312 | plugin.registerEditorExtension([textMatchMarker]);
313 | });
314 | }
315 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18next from "i18next";
2 | import { moment } from "obsidian";
3 | import * as en from "./locale/en.json";
4 | import * as zh_cn from "./locale/zh_cn.json";
5 | i18next.init({
6 | lng: "en", // if you're using a language detector, do not define the lng option
7 | fallbackLng: "en",
8 | debug: false,
9 | resources: {
10 | en: {
11 | translation: en,
12 | },
13 | zh_cn: {
14 | translation: zh_cn,
15 | },
16 | },
17 | });
18 | const locale = moment.locale();
19 | if (locale) {
20 | // zh-cn无法识别,改成下划线
21 | const lang = locale.replace("-", "_");
22 | i18next.changeLanguage(lang);
23 | }
24 | export const i18n = i18next;
25 |
--------------------------------------------------------------------------------
/src/i18n/locale/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "name": "Text Finder"
4 | },
5 | "search": {
6 | "tip": {
7 | "NoResults": "No results",
8 | "HasResults": "{{current}} of {{total}}",
9 | "ToggleReplace": "Toggle replace",
10 | "MatchCase": "Match case",
11 | "UseRegularExpression": "Use regular expression",
12 | "PreviousMatch": "Previous match",
13 | "NextMatch": "Next match",
14 | "Close": "Close (Escape)",
15 | "Replace": "Replace",
16 | "ReplaceAll": "Replace all",
17 | "FindPlaceholder": "Find",
18 | "ReplacePlaceholder": "Replace"
19 | }
20 | },
21 | "commands": {
22 | "ShowFind": {
23 | "name": "Open search in current file"
24 | },
25 | "ShowFindAndReplace": {
26 | "name": "Open search & replace in current file"
27 | },
28 | "HideFind": {
29 | "name": "Hide finder"
30 | },
31 | "ToggleReplace": {
32 | "name": "Toggle replacer"
33 | },
34 | "ToggleFind": {
35 | "name": "Toggle finder"
36 | },
37 | "PreviousMatch": {
38 | "name": "Previous match"
39 | },
40 | "NextMatch": {
41 | "name": "Next match"
42 | },
43 | "Replace": {
44 | "name": "Replace in current file"
45 | },
46 | "ReplaceAll": {
47 | "name": "Replace all in current file"
48 | }
49 | },
50 | "settings": {
51 | "ClearAfterHidden": {
52 | "name": "Clear search text",
53 | "desc": "When closing the search box, the search text will be cleared."
54 | },
55 | "EnableInputHotkeys": {
56 | "name": "Enable input box hotkey",
57 | "desc": "Add a hotkey (Enter) to the input box. Search: Go to the next matching item.Replace: Replace once."
58 | },
59 | "SourceModeWhenSearch": {
60 | "name": "Force source code mode during search",
61 | "desc": "Force the use of source code mode during the search process (some search results cannot be directly displayed in live preview, such as images and hyperlinks)."
62 | },
63 | "MoveCursorToMatch": {
64 | "name": "Move the cursor to the matching item",
65 | "desc": "Move the cursor and select the current match when searching."
66 | },
67 | "UseSelectionAsSearch": {
68 | "name": "Use selection as search text",
69 | "desc": "Use the selected text as the search text."
70 | },
71 | "UseObsidianSearchInRead": {
72 | "name": "Using Obsidian's native search in reading",
73 | "desc": "In reading mode, using the command \"{{name}}\" will invoke Obsidian's native search function(The search and replace provided by the plugin are only applicable to editing mode)"
74 | },
75 | "UseEscapeCharInReplace": {
76 | "name": "Support escape characters in replacement input box",
77 | "desc": "In regular expression mode, escape characters for line feed (LF) and horizontal tab (HT) can be used in the replacement input box."
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/i18n/locale/zh_cn.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugin": {
3 | "name": "Text Finder"
4 | },
5 | "search": {
6 | "tip": {
7 | "NoResults": "无结果",
8 | "HasResults": "第{{current}}项,共{{total}}项",
9 | "ToggleReplace": "切换替换",
10 | "MatchCase": "区分大小写",
11 | "UseRegularExpression": "使用正则表达式",
12 | "PreviousMatch": "上一个匹配项",
13 | "NextMatch": "下一个匹配项",
14 | "Close": "关闭 (Escape)",
15 | "Replace": "替换",
16 | "ReplaceAll": "全部替换",
17 | "FindPlaceholder": "查找",
18 | "ReplacePlaceholder": "替换"
19 | }
20 | },
21 | "commands": {
22 | "ShowFind": {
23 | "name": "在当前文件中:打开查找"
24 | },
25 | "ShowFindAndReplace": {
26 | "name": "在当前文件中:打开查找与替换"
27 | },
28 | "HideFind": {
29 | "name": "关闭查找窗口"
30 | },
31 | "ToggleReplace": {
32 | "name": "切换替换窗口"
33 | },
34 | "ToggleFind": {
35 | "name": "切换查找窗口"
36 | },
37 | "PreviousMatch": {
38 | "name": "上一个匹配"
39 | },
40 | "NextMatch": {
41 | "name": "下一个匹配"
42 | },
43 | "Replace": {
44 | "name": "在当前文件中:替换"
45 | },
46 | "ReplaceAll": {
47 | "name": "在当前文件中:替换所有"
48 | }
49 | },
50 | "settings": {
51 | "ClearAfterHidden": {
52 | "name": "清空搜索文本",
53 | "desc": "在关闭搜索窗口后清空搜索文本"
54 | },
55 | "EnableInputHotkeys": {
56 | "name": "启用输入框的快捷键",
57 | "desc": "为输入框添加快捷键(Enter)。搜索:到下一个匹配项,替换:替换一次"
58 | },
59 | "SourceModeWhenSearch": {
60 | "name": "搜索时强制源代码模式",
61 | "desc": "在搜索过程中强制使用源代码模式(某些搜索结果无法直接显示在实时预览中,如图像和超链接)"
62 | },
63 | "MoveCursorToMatch": {
64 | "name": "将光标移动至匹配项",
65 | "desc": "搜索时移动光标并选择当前匹配项"
66 | },
67 | "UseSelectionAsSearch": {
68 | "name": "将所选内容用作搜索文本",
69 | "desc": "将所选文本用作搜索文本"
70 | },
71 | "UseObsidianSearchInRead": {
72 | "name": "阅读模式下使用Obsidian搜索",
73 | "desc": "在阅读模式下,使用命令 \"{{name}}\" 将会调用Obsidian原生的搜索功能(插件提供的搜索与替换仅适用于编辑模式)"
74 | },
75 | "UseEscapeCharInReplace": {
76 | "name": "支持替换输入框中使用转义字符",
77 | "desc": "在正则表达式模式下,替换输入框中可以使用换行(LF)和水平制表符(HT)的转义字符"
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { App, Plugin, PluginSettingTab, Setting } from "obsidian";
2 | import { i18n } from "./i18n";
3 | import { editorExtensionProvider, EditorSearch } from "./editor-extension";
4 |
5 | interface PluginSettings {
6 | /**
7 | * 隐藏窗口时清空输入项
8 | */
9 | clearAfterHidden: boolean;
10 | /**
11 | * 启用输入框的快捷键
12 | */
13 | enableInputHotkeys: boolean;
14 | /**
15 | * 在搜索启用时,强制进入source模式.在退出后,根据是否开启预览模式切换
16 | */
17 | sourceModeWhenSearch: boolean;
18 | /**
19 | * 是否移动光标至匹配项
20 | */
21 | moveCursorToMatch: boolean;
22 | /**
23 | * 是否使用选中文本作为搜索文本
24 | */
25 | useSelectionAsSearch: boolean;
26 | /**
27 | * 在阅读模式下,命令会调用Obsidian的搜索
28 | */
29 | useObsidianSearchInRead: boolean;
30 | /**
31 | * 在替换框中支持部分转义字符串:\n \t
32 | */
33 | useEscapeCharInReplace: boolean;
34 | }
35 |
36 | const DEFAULT_SETTINGS: PluginSettings = {
37 | clearAfterHidden: false,
38 | enableInputHotkeys: true,
39 | sourceModeWhenSearch: true,
40 | moveCursorToMatch: true,
41 | useSelectionAsSearch: true,
42 | useObsidianSearchInRead: true,
43 | useEscapeCharInReplace: true,
44 | };
45 |
46 | export default class TextFinderPlugin extends Plugin {
47 | settings!: PluginSettings;
48 | editorSearch: EditorSearch | null = null;
49 | async onload() {
50 | await this.loadSettings();
51 | this.addSettingTab(new SettingTab(this.app, this));
52 |
53 | editorExtensionProvider(this);
54 | }
55 | onunload() {
56 | // 在取消加载插件的时候销毁finder的svelte组件,不然重复开关会重复创建,虽然没有影响
57 | this.editorSearch?.destoryAll();
58 | }
59 | async loadSettings() {
60 | this.settings = Object.assign(
61 | {},
62 | DEFAULT_SETTINGS,
63 | await this.loadData()
64 | );
65 | }
66 |
67 | async saveSettings() {
68 | await this.saveData(this.settings);
69 | }
70 | }
71 |
72 | class SettingTab extends PluginSettingTab {
73 | plugin: TextFinderPlugin;
74 |
75 | constructor(app: App, plugin: TextFinderPlugin) {
76 | super(app, plugin);
77 | this.plugin = plugin;
78 | }
79 |
80 | display(): void {
81 | const { containerEl } = this;
82 | containerEl.empty();
83 |
84 | const pluginSetting = this.plugin.settings;
85 | new Setting(containerEl)
86 | .setName(i18n.t("settings.ClearAfterHidden.name"))
87 | .setDesc(i18n.t("settings.ClearAfterHidden.desc"))
88 | .addToggle((cb) => {
89 | cb.setValue(pluginSetting.clearAfterHidden).onChange(
90 | async (value: boolean) => {
91 | pluginSetting.clearAfterHidden = value;
92 | await this.plugin.saveSettings();
93 | }
94 | );
95 | });
96 |
97 | new Setting(containerEl)
98 | .setName(i18n.t("settings.EnableInputHotkeys.name"))
99 | .setDesc(i18n.t("settings.EnableInputHotkeys.desc"))
100 | .addToggle((cb) => {
101 | cb.setValue(pluginSetting.enableInputHotkeys).onChange(
102 | async (value: boolean) => {
103 | pluginSetting.enableInputHotkeys = value;
104 | await this.plugin.saveSettings();
105 | }
106 | );
107 | });
108 |
109 | new Setting(containerEl)
110 | .setName(i18n.t("settings.SourceModeWhenSearch.name"))
111 | .setDesc(i18n.t("settings.SourceModeWhenSearch.desc"))
112 | .addToggle((cb) => {
113 | cb.setValue(pluginSetting.sourceModeWhenSearch).onChange(
114 | async (value: boolean) => {
115 | pluginSetting.sourceModeWhenSearch = value;
116 | await this.plugin.saveSettings();
117 | }
118 | );
119 | });
120 |
121 | new Setting(containerEl)
122 | .setName(i18n.t("settings.MoveCursorToMatch.name"))
123 | .setDesc(i18n.t("settings.MoveCursorToMatch.desc"))
124 | .addToggle((cb) => {
125 | cb.setValue(pluginSetting.moveCursorToMatch).onChange(
126 | async (value: boolean) => {
127 | pluginSetting.moveCursorToMatch = value;
128 | await this.plugin.saveSettings();
129 | }
130 | );
131 | });
132 |
133 | new Setting(containerEl)
134 | .setName(i18n.t("settings.UseSelectionAsSearch.name"))
135 | .setDesc(i18n.t("settings.UseSelectionAsSearch.desc"))
136 | .addToggle((cb) => {
137 | cb.setValue(pluginSetting.useSelectionAsSearch).onChange(
138 | async (value: boolean) => {
139 | pluginSetting.useSelectionAsSearch = value;
140 | await this.plugin.saveSettings();
141 | }
142 | );
143 | });
144 |
145 | new Setting(containerEl)
146 | .setName(i18n.t("settings.UseObsidianSearchInRead.name"))
147 | .setDesc(
148 | i18n.t("settings.UseObsidianSearchInRead.desc", {
149 | name:
150 | i18n.t("plugin.name") +
151 | ": " +
152 | i18n.t("commands.ShowFind.name"),
153 | })
154 | )
155 | .addToggle((cb) => {
156 | cb.setValue(pluginSetting.useObsidianSearchInRead).onChange(
157 | async (value: boolean) => {
158 | pluginSetting.useObsidianSearchInRead = value;
159 | await this.plugin.saveSettings();
160 | }
161 | );
162 | });
163 |
164 | new Setting(containerEl)
165 | .setName(i18n.t("settings.UseEscapeCharInReplace.name"))
166 | .setDesc(i18n.t("settings.UseEscapeCharInReplace.desc"))
167 | .addToggle((cb) => {
168 | cb.setValue(pluginSetting.useEscapeCharInReplace).onChange(
169 | async (value: boolean) => {
170 | pluginSetting.useEscapeCharInReplace = value;
171 | await this.plugin.saveSettings();
172 | }
173 | );
174 | });
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | --nya-focus-border-color: #2488db;
3 | --nya-dragger-color: #39c5bb;
4 | --nya-default-width: 448px;
5 | --nya-match-bgColor: #ea5c0054;
6 | --nya-match-color: #ffffff;
7 | --nya-match-curbgColor: #fafa00f2;
8 | --nya-match-curColor: #ff0000;
9 | }
10 | .nya-text-finder-match {
11 | background-color: var(--nya-match-bgColor);
12 | color: var(--nya-match-color);
13 | }
14 |
15 | .nya-text-finder-match-current {
16 | background-color: var(--nya-match-curbgColor);
17 | color: var(--nya-match-curColor);
18 | }
19 |
20 | .workspace-leaf-content[data-mode="preview"] {
21 | .nya-finder {
22 | visibility: hidden;
23 | user-select: none;
24 | pointer-events: none;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface MatchOffset {
4 | /**
5 | * 起始位置
6 | */
7 | from: number;
8 | /**
9 | * 起始位置
10 | */
11 | to: number;
12 | /**
13 | * 放入数组时的index
14 | */
15 | index: number;
16 | }
17 |
18 | interface SearchOptions {
19 | /**
20 | * 启用正则表达式模式
21 | */
22 | regexMode: boolean;
23 | /**
24 | * 启用大小写敏感
25 | */
26 | caseSensitive: boolean;
27 | }
28 |
29 | interface SearchCache {
30 | /**
31 | * 搜索文本
32 | */
33 | search: string;
34 | /**
35 | * 替换文本
36 | */
37 | replace: string;
38 | /**
39 | * 当前项索引
40 | */
41 | index: number;
42 | /**
43 | * 匹配项
44 | */
45 | matches: MatchOffset[];
46 | /**
47 | * 是否显示
48 | */
49 | visible: boolean;
50 | /**
51 | * 是否折叠
52 | */
53 | collapse: boolean;
54 | /**
55 | * 搜索选项
56 | */
57 | options: SearchOptions;
58 | }
59 |
60 | interface ReplaceResult {
61 | /**
62 | * 是否有进行替换
63 | */
64 | changed: boolean;
65 | /**
66 | * 替换时的搜索框文本
67 | */
68 | search: string;
69 | /**
70 | * 替换时的替换框文本
71 | */
72 | replace: string;
73 | /**
74 | * 影响的记录数
75 | */
76 | changeCount: number;
77 | /**
78 | * 替换前的匹配总数量
79 | */
80 | beforeCount: number;
81 | /**
82 | * 替换后的匹配总数量
83 | */
84 | afterCount: number;
85 | }
86 |
--------------------------------------------------------------------------------
/src/util/common-helper.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 生成一个唯一的ID,有UUID就用UUID,不然用时间戳+随机数
3 | * 对于唯一性其实没那么高要求
4 | * @returns 唯一ID
5 | */
6 | export function generateUniqueId() {
7 | if (crypto && crypto.randomUUID) {
8 | return crypto.randomUUID();
9 | } else {
10 | return generateSimpleId();
11 | }
12 | }
13 |
14 | /**
15 | * 生成一个简单的ID
16 | * @returns ID
17 | */
18 | export function generateSimpleId() {
19 | const timeStr = Date.now().toString(36);
20 | const randomStr = Math.random().toString(36).substring(2);
21 | return `${timeStr}-${randomStr}`;
22 | }
23 |
--------------------------------------------------------------------------------
/src/util/text-helper.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 从source中查找target的offset坐标数组
3 | * @param source source字符串
4 | * @param target target字符串
5 | * @param enableRegexMode 是否使用正则表达式
6 | * @param enableCaseSensitive 是否大小写敏感
7 | * @returns offset数组
8 | */
9 | export function findTextOffsets(
10 | source: string,
11 | target: string,
12 | enableRegexMode: boolean,
13 | enableCaseSensitive: boolean
14 | ): MatchOffset[] {
15 | const matches: MatchOffset[] = [];
16 | if (source != "" && target != "") {
17 | let index = 0;
18 | if (enableRegexMode) {
19 | try {
20 | const flags = "gm" + (enableCaseSensitive ? "" : "i");
21 | const regex = new RegExp(target, flags);
22 |
23 | // for (const match of source.matchAll(regex)) {
24 | // matches.push({
25 | // from: match.index,
26 | // to: match.index + match[0].length,
27 | // });
28 | // }
29 | let match;
30 | // 可能有一些正则表达式会导致无限循环,考虑弄个黑名单直接返回[]
31 |
32 | while ((match = regex.exec(source)) !== null) {
33 | if (match[0] == "") {
34 | regex.lastIndex++;
35 | } else {
36 | matches.push({
37 | from: match.index,
38 | to: match.index + match[0].length,
39 | index: index++,
40 | });
41 | }
42 | }
43 | } catch (error) {
44 | console.log(`Finder match error!`, error);
45 | return [];
46 | }
47 | } else {
48 | const targetStr = enableCaseSensitive
49 | ? target
50 | : target.toLowerCase();
51 | const sourceStr = enableCaseSensitive
52 | ? source
53 | : source.toLowerCase();
54 |
55 | let startIndex = 0;
56 | while (
57 | (startIndex = sourceStr.indexOf(targetStr, startIndex)) !== -1
58 | ) {
59 | const endIndex = startIndex + targetStr.length;
60 | matches.push({
61 | from: startIndex,
62 | to: endIndex,
63 | index: index++,
64 | });
65 | startIndex += targetStr.length;
66 | }
67 | }
68 | }
69 | return matches;
70 | }
71 |
72 | /**
73 | * 找出list中,位于target之后的第一个符合结果
74 | * @param target 目标Offset
75 | * @param list Offset数组
76 | * @param extraOffset 偏移量 额外偏移量
77 | * @returns index
78 | */
79 | export function findIndexAfterOffset(
80 | target: MatchOffset,
81 | list: MatchOffset[],
82 | extraOffset: number
83 | ): number {
84 | return list.findIndex((item) => item.to >= target.from + extraOffset);
85 | }
86 |
--------------------------------------------------------------------------------
/style-settings.css:
--------------------------------------------------------------------------------
1 | /* @settings
2 |
3 | name: Text Finder
4 | id: obsidian-text-finder
5 | settings:
6 | -
7 | id: nya-default-width
8 | title: Default width
9 | title.zh: 默认宽度
10 | description: Default width of the search window
11 | description.zh: 搜索窗口的默认宽度
12 | type: variable-text
13 | default: 448px
14 | -
15 | id: nya-focus-border-color
16 | title: Focused border color
17 | title.zh: 聚焦边框颜色
18 | description: The color of the border when focused
19 | description.zh: 控件聚焦时的边框颜色
20 | type: variable-color
21 | opacity: true
22 | format: hex
23 | default: '#2488db'
24 | -
25 | id: nya-dragger-color
26 | title: Dragger color
27 | title.zh: 拖拽控件的颜色
28 | description: The color of the dragger
29 | description.zh: 拖拽控件的颜色
30 | type: variable-color
31 | opacity: true
32 | format: hex
33 | default: '#39c5bb'
34 | -
35 | id: nya-match-bgColor
36 | title: Match item background color
37 | title.zh: 匹配项背景颜色
38 | description: The background color of the matched item
39 | description.zh: 匹配项的背景色
40 | type: variable-color
41 | opacity: true
42 | format: hex
43 | default: '#ea5c0054'
44 | -
45 | id: nya-match-color
46 | title: Match item text color
47 | title.zh: 匹配项文字颜色
48 | description: The text color of the matched item
49 | description.zh: 匹配项的文字颜色
50 | type: variable-color
51 | opacity: true
52 | format: hex
53 | default: '#ffffff'
54 | -
55 | id: nya-match-curbgColor
56 | title: Current item background color
57 | title.zh: 当前项背景颜色
58 | description: The background color of the current item
59 | description.zh: 当前项的背景色
60 | type: variable-color
61 | opacity: true
62 | format: hex
63 | default: '#fafa00f2'
64 | -
65 | id: nya-match-curColor
66 | title: Current item text color
67 | title.zh: 当前项文字颜色
68 | description: The text color of the current item
69 | description.zh: 当前项的文字颜色
70 | type: variable-color
71 | opacity: true
72 | format: hex
73 | default: '#ff0000'
74 | */
75 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "compilerOptions": {
4 | "types": ["svelte", "node"],
5 | "baseUrl": ".",
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "resolveJsonModule": true,
15 | "lib": ["DOM", "ES5", "ES6", "ES7"]
16 | },
17 | "include": ["**/*.ts", "**/*.svelte", "**/*.d.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.1.0": "0.15.0"
3 | }
4 |
--------------------------------------------------------------------------------