├── .github └── workflows │ └── release-please.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── ui.png ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── publish └── gif │ ├── settings-search-enter.gif │ ├── settings-search-keyboard.gif │ ├── settings-search-settings-search-highlight.gif │ └── settings-search-up-down.gif ├── release-please-config.json ├── src ├── assets │ └── main.css ├── main.ts └── styles.css ├── svelte.config.js ├── tsconfig.json └── versions.json /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | push: 5 | branches: 6 | - "main" 7 | env: 8 | PLUGIN_NAME: settings-search 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | name: release-please 15 | 16 | jobs: 17 | release-please: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | release_created: ${{ steps.release.outputs.release_created }} 21 | upload_url: ${{ steps.release.outputs.upload_url }} 22 | tag_name: ${{ steps.release.outputs.tag_name }} 23 | steps: 24 | - uses: google-github-actions/release-please-action@v3 25 | id: release 26 | with: 27 | command: manifest 28 | 29 | upload-build: 30 | runs-on: ubuntu-latest 31 | needs: release-please 32 | if: ${{ needs.release-please.outputs.release_created }} 33 | env: 34 | upload_url: ${{ needs.release-please.outputs.upload_url }} 35 | tag_name: ${{ needs.release-please.outputs.tag_name }} 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Use Node.js 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: "18.x" # You might need to adjust this value to your own version 42 | - name: Build 43 | id: build 44 | run: | 45 | npm install 46 | npm run build --if-present 47 | mkdir ${{ env.PLUGIN_NAME }} 48 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 49 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 50 | - name: Upload zip file 51 | id: upload-zip 52 | uses: actions/upload-release-asset@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ env.upload_url }} 57 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 58 | asset_name: ${{ env.PLUGIN_NAME }}-${{ env.tag_name }}.zip 59 | asset_content_type: application/zip 60 | - name: Upload main.js 61 | id: upload-main 62 | uses: actions/upload-release-asset@v1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | with: 66 | upload_url: ${{ env.upload_url }} 67 | asset_path: ./main.js 68 | asset_name: main.js 69 | asset_content_type: text/javascript 70 | - name: Upload manifest.json 71 | id: upload-manifest 72 | uses: actions/upload-release-asset@v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | upload_url: ${{ env.upload_url }} 77 | asset_path: ./manifest.json 78 | asset_name: manifest.json 79 | asset_content_type: application/json 80 | - name: Upload styles.css 81 | id: upload-css 82 | uses: actions/upload-release-asset@v1 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | with: 86 | upload_url: ${{ env.upload_url }} 87 | asset_path: ./styles.css 88 | asset_name: styles.css 89 | asset_content_type: text/css 90 | notify: 91 | needs: upload-build 92 | uses: javalent/workflows/.github/workflows/notify.yml@main 93 | secrets: inherit 94 | with: 95 | name: Settings Search 96 | repo: settings-search 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | 8 | # build 9 | main.js 10 | *.js.map 11 | dist 12 | styles.css 13 | 14 | # obsidian 15 | data.json 16 | .DS_Store 17 | *dev.js 18 | main.js.LICENSE.txt 19 | 20 | .env -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.3.10" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.3.10](https://github.com/javalent/settings-search/compare/1.3.9...1.3.10) (2023-10-05) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * fix error that occurs when plugins are loaded after this one (close [#43](https://github.com/javalent/settings-search/issues/43), close [#44](https://github.com/javalent/settings-search/issues/44)) ([2581772](https://github.com/javalent/settings-search/commit/2581772ad366e6659bd225aafc09e9d9f43c0c51)) 11 | 12 | ## [1.3.9](https://github.com/javalent/settings-search/compare/1.3.8...1.3.9) (2023-10-03) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * Keep track of viewed plugin setting tabs ([65eb91c](https://github.com/javalent/settings-search/commit/65eb91cb5a734fd137b8e22ed46690300c48f77d)) 18 | 19 | ## [1.3.8](https://github.com/javalent/settings-search/compare/1.3.7...1.3.8) (2023-05-30) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **Readme:** Update Readme to 1.0 ([ecefe4d](https://github.com/javalent/settings-search/commit/ecefe4d1103a597e4949f9a8c3b73575c7fd6012)), closes [#35](https://github.com/javalent/settings-search/issues/35) 25 | 26 | ### [1.3.7](https://github.com/valentine195/obsidian-settings-search/compare/1.3.6...1.3.7) (2023-03-20) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * adds Ctrl+P / Ctrl+N to navigate results (close [#32](https://github.com/valentine195/obsidian-settings-search/issues/32)) ([44a4c99](https://github.com/valentine195/obsidian-settings-search/commit/44a4c995fb9a4ba4e24872f346a484afe678e0d3)) 32 | 33 | ### [1.3.5](https://github.com/valentine195/obsidian-settings-search/compare/1.3.4...1.3.5) (2023-01-20) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * adds setting search loaded event ([ae4c2fb](https://github.com/valentine195/obsidian-settings-search/commit/ae4c2fb5a1572feb76fbfe540653f15e659e7157)) 39 | 40 | ### [1.3.3](https://github.com/valentine195/obsidian-settings-search/compare/1.3.2...1.3.3) (2023-01-20) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * expose removeTabResources ([8eb6990](https://github.com/valentine195/obsidian-settings-search/commit/8eb6990c92b6e08cca95e1f9f76d30848ffb9754)) 46 | 47 | ### [1.3.2](https://github.com/valentine195/obsidian-settings-search/compare/1.3.1...1.3.2) (2023-01-20) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * addResource -> addResources, removeResource -> removeResources, addResources now returns a deregister function ([1afdb45](https://github.com/valentine195/obsidian-settings-search/commit/1afdb45a8d5f4edba2c7e096d2de756bf7f29169)) 53 | 54 | ### [1.3.1](https://github.com/valentine195/obsidian-settings-search/compare/1.3.0...1.3.1) (2023-01-20) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * adds a way to remove external resources ([fe72419](https://github.com/valentine195/obsidian-settings-search/commit/fe724191a3b57d1d3558f5a024c0892aa3c2bd22)) 60 | 61 | ## [1.3.0](https://github.com/valentine195/obsidian-settings-search/compare/1.2.0...1.3.0) (2023-01-19) 62 | 63 | 64 | ### Features 65 | 66 | * adds ability for other plugins to add settings ([f35ee83](https://github.com/valentine195/obsidian-settings-search/commit/f35ee83d98ccb1f504d08ea7674895b7414a6403)) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * fixes jump to setting ([27e2b41](https://github.com/valentine195/obsidian-settings-search/commit/27e2b4150df338087d43fc67a6b14c3f010a6a9b)) 72 | 73 | ## [1.2.0](https://github.com/valentine195/obsidian-settings-search/compare/1.1.0...1.2.0) (2022-03-23) 74 | 75 | 76 | ### Features 77 | 78 | * Settings Search is now on mobile! closes [#9](https://github.com/valentine195/obsidian-settings-search/issues/9) ([8838d41](https://github.com/valentine195/obsidian-settings-search/commit/8838d41f41428d89cdead96bd68a692834aa446c)) 79 | 80 | ## [1.1.0](https://github.com/valentine195/obsidian-settings-search/compare/1.0.7...1.1.0) (2022-03-17) 81 | 82 | 83 | ### Features 84 | 85 | * Added keyboard navigation to search results (close [#3](https://github.com/valentine195/obsidian-settings-search/issues/3)) ([f178598](https://github.com/valentine195/obsidian-settings-search/commit/f17859842d8d79237e9c64a6a1df818fb31dfa94)) 86 | 87 | ### [1.0.7](https://github.com/valentine195/obsidian-settings-search/compare/1.0.6...1.0.7) (2022-03-15) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * Wait for SettingTab display call before grabbing settings (close [#13](https://github.com/valentine195/obsidian-settings-search/issues/13)) ([b1adc7f](https://github.com/valentine195/obsidian-settings-search/commit/b1adc7f35a36bfe6424e88665e418bce66b1ff2c)) 93 | 94 | ### [1.0.6](https://github.com/valentine195/obsidian-settings-search/compare/1.0.5...1.0.6) (2022-03-10) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * Adds check for nested details elements (close [#11](https://github.com/valentine195/obsidian-settings-search/issues/11)) ([0d07279](https://github.com/valentine195/obsidian-settings-search/commit/0d072796f9613357a8462a98431db89d2b7e4f29)) 100 | * Fixes padding issue on default theme (close [#12](https://github.com/valentine195/obsidian-settings-search/issues/12)) ([13a3e98](https://github.com/valentine195/obsidian-settings-search/commit/13a3e985519de3cc01ee4b61f55e3e0b53d03a03)) 101 | 102 | ### [1.0.5](https://github.com/valentine195/obsidian-settings-search/compare/1.0.4...1.0.5) (2022-03-03) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * Hotkeys are now always at the bottom of results (close [#10](https://github.com/valentine195/obsidian-settings-search/issues/10)) ([625d64e](https://github.com/valentine195/obsidian-settings-search/commit/625d64e4af28c559a017c1d07075d1f0e3c3fefd)) 108 | 109 | ### [1.0.4](https://github.com/valentine195/obsidian-settings-search/compare/1.0.3...1.0.4) (2022-02-23) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * Fixed settings search placeholder ([a71e4bb](https://github.com/valentine195/obsidian-settings-search/commit/a71e4bb99fdf1aa40ddfa17c0e64d79bcd5cc2b5)) 115 | 116 | ### [1.0.3](https://github.com/valentine195/obsidian-settings-search/compare/1.0.2...1.0.3) (2022-02-23) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * Loading plugin no longer closes the active settings tab ([317c1e2](https://github.com/valentine195/obsidian-settings-search/commit/317c1e2ad104f36d044c1c9ecc8e0182a0c16c96)) 122 | * Settings indexing is no longer blocks the main process ([ee7b7b9](https://github.com/valentine195/obsidian-settings-search/commit/ee7b7b947cf3e00c987e122141e5ab19155a830b)) 123 | * Unloading the plugin no longer re-loads the active settings tab ([5579a8d](https://github.com/valentine195/obsidian-settings-search/commit/5579a8d5d55a85465431509e9ce33da3040707fc)) 124 | 125 | ### [1.0.2](https://github.com/valentine195/obsidian-settings-search/compare/1.0.1...1.0.2) (2022-02-17) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * call hide() on settings tabs ([78ea4c9](https://github.com/valentine195/obsidian-settings-search/commit/78ea4c9fc9de8bd1307607ec9ae9c27cf3429c8f)) 131 | * calls tab.hide after building settings tab resources to properly unload ([7de69f6](https://github.com/valentine195/obsidian-settings-search/commit/7de69f65d5e5fe040c090199f45af32a7fcd6010)) 132 | 133 | ### [1.0.1](https://github.com/valentine195/obsidian-settings-search/compare/1.0.0...1.0.1) (2022-02-15) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * fixed issue where settings with no descriptions would break fuzzy search (close [#6](https://github.com/valentine195/obsidian-settings-search/issues/6)) ([fecf025](https://github.com/valentine195/obsidian-settings-search/commit/fecf02500f90437487aed33133c1cf4ae1ad3b24)) 139 | 140 | ## [1.0.0](https://github.com/valentine195/obsidian-settings-search/compare/0.0.2...1.0.0) (2022-02-14) 141 | 142 | 143 | ### Features 144 | 145 | * can now open plugin settings directly from search results (close [#4](https://github.com/valentine195/obsidian-settings-search/issues/4)) ([a0d6b6f](https://github.com/valentine195/obsidian-settings-search/commit/a0d6b6f591b0126243287cb09fc407ee5398b2b9)) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * patches setting open to focus search field on open (close [#2](https://github.com/valentine195/obsidian-settings-search/issues/2)) ([1b6561e](https://github.com/valentine195/obsidian-settings-search/commit/1b6561e690061df0eaf535ee55f336fd369a2378)) 151 | * settings descriptions are now recreated as-is (closes [#5](https://github.com/valentine195/obsidian-settings-search/issues/5)) ([2b3678c](https://github.com/valentine195/obsidian-settings-search/commit/2b3678c8730fd0d7fd5fe8dbffa413e2e58b0f1d)) 152 | 153 | ### 0.0.2 (2022-02-10) 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 valentine195 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.md: -------------------------------------------------------------------------------- 1 | > 🥇 Our documentation has moved ***[here](https://plugins.javalent.com/settings-search)***. 2 | > 3 | > Buy Me a Coffee at ko-fi.com 4 | > 5 | > --- 6 | > 7 | > **Development Status**: Maintenance Mode 8 | > 9 | > Due to a glut of high priority Javalent plugin projects, this plugin is now entering maintenance mode for the time being. This is **not** a permanent status. 10 | > - PR's will be reviewed. 11 | > - *Yay* bugs will be reviewed and worked if able. 12 | > - Feature Requests **will not** be worked. 13 | 14 | The Settings Search plugin adds global search functionality to the Obsidian settings, allowing users to quickly search for a particular setting. Keyboard navigation support is provided for ease of use. 15 | Additionally, plugin authors can add their settings dynamically using the API provided by the plugin. 16 | The plugin API includes three functions for this purpose: addResources(), removeResources(), and removeTabResources(). 17 | 18 | ## Features 19 | - Global search functionality added to the Obsidian settings. 20 | - Keyboard navigation support using the up and down arrow keys to move through search results and enter to go to the setting. 21 | - Dynamic settings rendering support using the API for plugin authors to add their settings dynamically. 22 | 23 | ### Quickstart 24 | 25 | 1. Install the plugin from the Obsidian community plugins browser. 26 | 2. Go to Obsidian Settings 27 | 3. Search! That's it. That's the plugin. 28 | 29 | Check out the **[plugin documentation](https://plugins.javalent.com/settings-search)** for more detailed instructions and examples. 30 | 31 | ## Support 32 | 33 | If you encounter any issues, want to give back and help out, or have suggestions for new features, file an issue on the **[GitHub repository](https://github.com/javalent/settings-search)**. 34 | 35 | ### Complementary plugins 36 | If you're using Obsidian to better organize your notes, you may find some of my other plugins useful: 37 | 38 | - [Admonitions](https://github.com/javalent/admonitions) Create and customize code-block callouts. 39 | - [Prominent Bookmarked Files](https://github.com/javalent/prominent-files) Make your Bookmarked files stand out better on the file pane. 40 | - [Markdown Attributes](https://github.com/javalent/markdown-attributes) Add classes and IDs to Markdown elements. 41 | -------------------------------------------------------------------------------- /assets/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalent/settings-search/8f9090ef354b9e927b6fe0ff6b7147618b531702/assets/ui.png -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | import { config } from "dotenv"; 5 | 6 | config(); 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 dir = prod ? "./" : process.env.OUTDIR; 17 | 18 | esbuild 19 | .build({ 20 | banner: { 21 | js: banner 22 | }, 23 | entryPoints: ["src/main.ts", "src/styles.css"], 24 | bundle: true, 25 | external: ["obsidian", "electron", ...builtins], 26 | format: "cjs", 27 | watch: !prod, 28 | target: "es2020", 29 | logLevel: "info", 30 | sourcemap: prod ? false : "inline", 31 | treeShaking: true, 32 | outdir: dir 33 | }) 34 | .catch(() => process.exit(1)); 35 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "settings-search", 3 | "name": "Settings Search", 4 | "version": "1.3.10", 5 | "minAppVersion": "0.12.17", 6 | "author": "Jeremy Valentine", 7 | "description": "Globally search settings in Obsidian.md", 8 | "authorUrl": "https://github.com/valentine195", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "settings-search", 3 | "version": "1.3.10", 4 | "description": "Globally search settings in Obsidian.md", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "node esbuild.config.mjs production", 9 | "web:dev": "set NODE_OPTIONS=--openssl-legacy-provider && webpack --config webpack.dev.js -w", 10 | "web:build": "webpack", 11 | "rollup:dev": "rollup --config rollup.config-dev.js -w", 12 | "rollup:build": "rollup --config rollup.config.js", 13 | "test": "jest", 14 | "release": "standard-version", 15 | "push": "git push --follow-tags origin" 16 | }, 17 | "standard-version": { 18 | "t": "", 19 | "releaseCommitMessageFormat": "Settings Search Release: v{{currentTag}}" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@babel/core": "^7.15.8", 26 | "@babel/preset-env": "^7.15.8", 27 | "@types/node": "^14.17.21", 28 | "builtin-modules": "^3.2.0", 29 | "dotenv": "^10.0.0", 30 | "esbuild": "^0.13.15", 31 | "monkey-around": "^2.3.0", 32 | "obsidian": "latest", 33 | "standard-version": "^9.3.2", 34 | "tslib": "^2.3.0", 35 | "typescript": "^4.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /publish/gif/settings-search-enter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalent/settings-search/8f9090ef354b9e927b6fe0ff6b7147618b531702/publish/gif/settings-search-enter.gif -------------------------------------------------------------------------------- /publish/gif/settings-search-keyboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalent/settings-search/8f9090ef354b9e927b6fe0ff6b7147618b531702/publish/gif/settings-search-keyboard.gif -------------------------------------------------------------------------------- /publish/gif/settings-search-settings-search-highlight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalent/settings-search/8f9090ef354b9e927b6fe0ff6b7147618b531702/publish/gif/settings-search-settings-search-highlight.gif -------------------------------------------------------------------------------- /publish/gif/settings-search-up-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javalent/settings-search/8f9090ef354b9e927b6fe0ff6b7147618b531702/publish/gif/settings-search-up-down.gif -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "changelog-path": "CHANGELOG.md", 5 | "release-type": "node" 6 | } 7 | }, 8 | "include-component-in-tag": false, 9 | "include-v-in-tag": false, 10 | "extra-files": [ 11 | { 12 | "type": "json", 13 | "path": "manifest.json", 14 | "jsonpath": "$.version" 15 | } 16 | ], 17 | "last-release-sha": "ab8efeaa667ab0b88d2af0ca37c686b526ea1ee0", 18 | 19 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | .settings-search-container.vertical-tab-header-group { 2 | padding-bottom: 0; 3 | } 4 | .settings-search-input { 5 | padding-left: 6px; 6 | } 7 | .vertical-tab-nav-item.settings-search-input { 8 | background-color: inherit !important; 9 | } 10 | .settings-search-input .setting-item-control { 11 | display: block; 12 | } 13 | .settings-search-input .search-input-container { 14 | margin: 0; 15 | } 16 | 17 | .settings-search-results .setting-item.active { 18 | background-color: var(--background-secondary); 19 | } 20 | 21 | .settings-search-results .set-externally .setting-item-name { 22 | display: flex; 23 | gap: 0.5rem; 24 | } 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Plugin, 3 | SearchComponent, 4 | Setting, 5 | SearchResult, 6 | Notice, 7 | prepareSimpleSearch, 8 | SettingTab, 9 | Scope, 10 | Platform, 11 | setIcon 12 | } from "obsidian"; 13 | 14 | import { around } from "monkey-around"; 15 | 16 | declare module "obsidian" { 17 | interface App { 18 | internalPlugins: { 19 | plugins: Record< 20 | string, 21 | { _loaded: boolean; instance: { name: string; id: string } } 22 | >; 23 | }; 24 | plugins: { 25 | manifests: Record; 26 | plugins: Record; 27 | getPlugin(id: string): Plugin; 28 | }; 29 | setting: { 30 | onOpen(): void; 31 | onClose(): void; 32 | 33 | openTabById(id: string): void; 34 | openTab(tab: SettingTab): void; 35 | 36 | isPluginSettingTab(tab: SettingTab): boolean; 37 | addSettingTab(tab: SettingTab): void; 38 | removeSettingTab(tab: SettingTab): void; 39 | 40 | activeTab: SettingTab; 41 | lastTabId: string; 42 | 43 | pluginTabs: PluginSettingTab[]; 44 | settingTabs: SettingTab[]; 45 | 46 | tabContentContainer: HTMLDivElement; 47 | tabHeadersEl: HTMLDivElement; 48 | }; 49 | } 50 | interface Plugin { 51 | _loaded: boolean; 52 | } 53 | interface PluginSettingTab { 54 | name: string; 55 | } 56 | interface SettingTab { 57 | id: string; 58 | name: string; 59 | navEl: HTMLElement; 60 | } 61 | interface Workspace { 62 | on( 63 | name: "settings-search-loaded", 64 | callback: (...args: any[]) => any 65 | ): EventRef; 66 | } 67 | } 68 | declare global { 69 | interface Window { 70 | SettingsSearch?: { 71 | addResources: SettingsSearch["addResources"]; 72 | removeResources: SettingsSearch["removeResources"]; 73 | removeTabResources: SettingsSearch["removeTabResources"]; 74 | }; 75 | } 76 | } 77 | 78 | interface Resource { 79 | tab: string; 80 | //tab name 81 | name: string; 82 | text: string; 83 | desc: string; 84 | external?: boolean; 85 | } 86 | export default class SettingsSearch extends Plugin { 87 | settingsSearchEl: HTMLDivElement = createDiv( 88 | "settings-search-container vertical-tab-header-group" 89 | ); 90 | settingsResultsContainerEl = createDiv( 91 | "settings-search-results-container vertical-tab-content" 92 | ); 93 | settingsNavItemContainer = this.settingsSearchEl 94 | .createDiv("vertical-tab-header-group-items") 95 | .createDiv("vertical-tab-nav-item settings-search-input"); 96 | settingsResultsEl: HTMLDivElement; 97 | search: SearchComponent; 98 | locale: string; 99 | 100 | resources: Resource[] = []; 101 | results: Resource[] = []; 102 | 103 | async onload() { 104 | (window["SettingsSearch"] = { 105 | addResources: this.addResources.bind(this), 106 | removeResources: this.removeResources.bind(this), 107 | removeTabResources: this.removeTabResources.bind(this) 108 | }) && this.register(() => delete window["SettingsSearch"]); 109 | 110 | this.app.workspace.onLayoutReady(async () => { 111 | this.settingsResultsContainerEl.createEl("h3", { 112 | text: "Settings Search Results" 113 | }); 114 | this.settingsResultsEl = this.settingsResultsContainerEl.createDiv( 115 | "settings-search-results" 116 | ); 117 | 118 | this.buildScope(); 119 | this.buildSearch(); 120 | this.buildResources(); 121 | this.buildPluginResources(); 122 | this.patchSettings(); 123 | this.loaded = true; 124 | this.app.workspace.trigger("settings-search-loaded"); 125 | }); 126 | } 127 | loaded = false; 128 | tabIndex = 0; 129 | pluginTabIndex = 0; 130 | seen: string[] = []; 131 | buildResources() { 132 | const tab = this.app.setting.settingTabs[this.tabIndex]; 133 | if (tab && tab.id !== undefined && (!this.seen.includes(tab.id))) { 134 | this.getTabResources(tab); 135 | this.tabIndex++; 136 | setTimeout(() => this.buildResources()); 137 | } 138 | } 139 | buildPluginResources() { 140 | const tab = this.app.setting.pluginTabs[this.pluginTabIndex]; 141 | if (tab) { 142 | this.getTabResources(tab); 143 | this.pluginTabIndex++; 144 | setTimeout(() => this.buildPluginResources()); 145 | } 146 | } 147 | get manifests() { 148 | return Object.values(this.app.plugins.manifests); 149 | } 150 | settingCache: Map = new Map(); 151 | public addResourceToCache(resource: Resource) { 152 | if (!resource || !resource.text || !resource.name || !resource.tab) { 153 | return new Error("A valid resource must be provided."); 154 | } 155 | 156 | let name: DocumentFragment | string; 157 | if (resource.external) { 158 | name = createFragment((el) => { 159 | setIcon( 160 | el.createSpan({ 161 | attr: { 162 | "aria-label": 163 | "This setting was added by another plugin." 164 | } 165 | }), 166 | "info" 167 | ); 168 | el.createSpan({ text: resource.text }); 169 | }); 170 | } else { 171 | name = resource.text; 172 | } 173 | const setting = new Setting(createDiv()) 174 | .setName(name) 175 | .setDesc( 176 | createFragment( 177 | (e) => (e.createDiv().innerHTML = resource.desc ?? "") 178 | ) 179 | ); 180 | if (resource.external) { 181 | setting.settingEl.addClass("set-externally"); 182 | } 183 | if (resource.tab == "community-plugins") { 184 | let plugin = this.manifests.find((p) => p.name == resource.text); 185 | if ( 186 | plugin && 187 | this.app.plugins.getPlugin(plugin.id)?._loaded && 188 | this.app.setting.pluginTabs.find((t) => t.id == plugin.id) 189 | ) { 190 | setting.addExtraButton((b) => { 191 | b.setTooltip(`Open ${resource.text} Settings`).onClick( 192 | () => { 193 | this.app.setting.openTabById(plugin.id); 194 | } 195 | ); 196 | }); 197 | } 198 | } 199 | if (resource.tab == "plugins") { 200 | const plugins = Object.values(this.app.internalPlugins.plugins); 201 | const plugin = plugins.find( 202 | (p) => p._loaded && p.instance.name == resource.text 203 | ); 204 | 205 | if ( 206 | plugin && 207 | this.app.setting.pluginTabs.find( 208 | (t) => t.id == plugin.instance.id 209 | ) 210 | ) { 211 | setting.addExtraButton((b) => { 212 | b.setTooltip(`Open ${resource.text} Settings`).onClick( 213 | () => { 214 | this.app.setting.openTabById(plugin.instance.id); 215 | } 216 | ); 217 | }); 218 | } 219 | } 220 | setting.addExtraButton((b) => { 221 | b.setIcon("forward-arrow").onClick(() => { 222 | this.showResult(resource); 223 | }); 224 | }); 225 | this.settingCache.set(resource, setting); 226 | } 227 | getResourceFromCache(resource: Resource) { 228 | if (!this.settingCache.has(resource)) { 229 | this.addResourceToCache(resource); 230 | } 231 | return this.settingCache.get(resource); 232 | } 233 | removeResourcesFromCache(resources: Resource[]) { 234 | for (const resource of resources) { 235 | this.settingCache.delete(resource); 236 | } 237 | } 238 | addResources(...resources: Resource[]) { 239 | for (const resource of resources) { 240 | resource.external = true; 241 | if (this.resources.find((k) => this.equivalent(resource, k))) 242 | continue; 243 | this.resources.push(resource); 244 | this.addResourceToCache(resource); 245 | } 246 | return () => this.removeResources(...resources); 247 | } 248 | equivalent(resource1: Resource, resource2: Resource) { 249 | return ( 250 | resource1.name == resource2.name && 251 | resource1.tab == resource2.tab && 252 | resource1.text == resource2.text && 253 | resource1.desc == resource2.desc && 254 | resource1.external == resource2.external 255 | ); 256 | } 257 | removeResources(...resources: Resource[]) { 258 | const removing = []; 259 | const keys = [...this.settingCache.keys()]; 260 | for (const resource of resources) { 261 | if ( 262 | !resource || 263 | !resource.text || 264 | !resource.name || 265 | !resource.tab 266 | ) { 267 | continue; 268 | } 269 | resource.external = true; 270 | this.resources = this.resources.filter( 271 | (r) => !this.equivalent(resource, r) 272 | ); 273 | removing.push( 274 | ...keys.filter( 275 | (k) => k == resource || this.equivalent(resource, k) 276 | ) 277 | ); 278 | } 279 | this.removeResourcesFromCache(removing); 280 | } 281 | removeTabResources(tab: string) { 282 | const removing = this.resources.filter((t) => t.tab == tab); 283 | this.resources = this.resources.filter((t) => t.tab != tab); 284 | this.removeResourcesFromCache(removing); 285 | } 286 | async getTabResources(tab: SettingTab) { 287 | await tab.display(); 288 | const settings = tab.containerEl.querySelectorAll( 289 | ".setting-item:not(.setting-item-header)" 290 | ); 291 | for (const el of Array.from(settings)) { 292 | const text = 293 | el.querySelector( 294 | ".setting-item-name" 295 | )?.textContent; 296 | if (!text) continue; 297 | 298 | const desc = 299 | el.querySelector(".setting-item-description") 300 | ?.innerHTML ?? ""; 301 | 302 | const resource = { 303 | tab: tab.id, 304 | name: tab.name, 305 | text, 306 | desc 307 | }; 308 | this.resources.push(resource); 309 | this.addResourceToCache(resource); 310 | } 311 | if (this.app.setting.activeTab?.id == tab.id) return; 312 | this.seen.push(tab.id); 313 | tab.containerEl.detach(); 314 | tab.hide(); 315 | } 316 | patchSettings() { 317 | const self = this; 318 | 319 | this.register( 320 | around(this.app.setting, { 321 | onOpen: function (next) { 322 | return function () { 323 | next.apply(this); 324 | if (!Platform.isMobile) self.search.inputEl.focus(); 325 | return next; 326 | }; 327 | } 328 | }) 329 | ); 330 | 331 | //Patch addSettingTab to capture changes to plugin settings. 332 | this.register( 333 | around(this.app.setting, { 334 | addSettingTab: function (next) { 335 | return function (tab: SettingTab) { 336 | if (tab && tab.id !== undefined && (!self.seen.includes(tab.id))) { 337 | self.getTabResources(tab); 338 | } 339 | return next.call(this, tab); 340 | }; 341 | } 342 | }) 343 | ); 344 | 345 | //Patch removeSettingTab to capture changes to plugin settings. 346 | this.register( 347 | around(this.app.setting, { 348 | removeSettingTab: function (next) { 349 | return function (tab: SettingTab) { 350 | if (this.isPluginSettingTab(tab)) { 351 | self.removeTabResources(tab.id); 352 | } 353 | return next.call(this, tab); 354 | }; 355 | } 356 | }) 357 | ); 358 | 359 | this.register( 360 | around(this.app.setting, { 361 | openTab: function (next) { 362 | return function (tab: SettingTab) { 363 | self.searchAppended = false; 364 | self.app.keymap.popScope(self.scope); 365 | return next.call(this, tab); 366 | }; 367 | }, 368 | openTabById: function (next) { 369 | return function (tab: string) { 370 | self.searchAppended = false; 371 | self.app.keymap.popScope(self.scope); 372 | return next.call(this, tab); 373 | }; 374 | }, 375 | onClose: function (next) { 376 | return function () { 377 | if (Platform.isMobile) { 378 | self.detach(); 379 | } 380 | return next.call(this); 381 | }; 382 | } 383 | }) 384 | ); 385 | } 386 | 387 | buildSearch() { 388 | const tempSetting = new Setting(createDiv()).addSearch((s) => { 389 | this.search = s; 390 | }); 391 | 392 | this.settingsNavItemContainer.append(tempSetting.controlEl); 393 | 394 | tempSetting.settingEl.detach(); 395 | 396 | this.search.onChange((v) => { 397 | this.onChange(v); 398 | }); 399 | this.search.setPlaceholder("Search settings..."); 400 | this.app.setting.tabHeadersEl.prepend(this.settingsSearchEl); 401 | } 402 | 403 | searchAppended = false; 404 | activeIndex = -1; 405 | activeSetting: Setting; 406 | scope = new Scope(this.app.scope); 407 | buildScope() { 408 | this.scope.register(["Ctrl"], "N", () => { 409 | if (this.activeSetting) { 410 | this.activeSetting.settingEl.removeClass("active"); 411 | } 412 | this.activeIndex = 413 | (((this.activeIndex + 1) % this.results.length) + 414 | this.results.length) % 415 | this.results.length; 416 | 417 | this.centerActiveSetting(); 418 | }); 419 | this.scope.register([], "ArrowDown", () => { 420 | if (this.activeSetting) { 421 | this.activeSetting.settingEl.removeClass("active"); 422 | } 423 | this.activeIndex = 424 | (((this.activeIndex + 1) % this.results.length) + 425 | this.results.length) % 426 | this.results.length; 427 | 428 | this.centerActiveSetting(); 429 | }); 430 | this.scope.register(["Ctrl"], "P", () => { 431 | if (this.activeSetting) { 432 | this.activeSetting.settingEl.removeClass("active"); 433 | } 434 | this.activeIndex = 435 | (((this.activeIndex - 1) % this.results.length) + 436 | this.results.length) % 437 | this.results.length; 438 | 439 | this.centerActiveSetting(); 440 | }); 441 | this.scope.register([], "ArrowUp", () => { 442 | if (this.activeSetting) { 443 | this.activeSetting.settingEl.removeClass("active"); 444 | } 445 | this.activeIndex = 446 | (((this.activeIndex - 1) % this.results.length) + 447 | this.results.length) % 448 | this.results.length; 449 | 450 | this.centerActiveSetting(); 451 | }); 452 | this.scope.register([], "Enter", () => { 453 | if (this.activeSetting) { 454 | this.showResult(this.results[this.activeIndex]); 455 | } 456 | }); 457 | } 458 | 459 | centerActiveSetting() { 460 | const result = this.results[this.activeIndex]; 461 | this.activeSetting = this.getResourceFromCache(result); 462 | this.activeSetting.settingEl.addClass("active"); 463 | 464 | this.activeSetting.settingEl.scrollIntoView({ 465 | behavior: "auto", 466 | block: "nearest" 467 | }); 468 | } 469 | mobileContainers: HTMLElement[] = []; 470 | detachFromMobile() { 471 | if (Platform.isMobile) { 472 | this.settingsResultsContainerEl.detach(); 473 | for (const header of this.mobileContainers) { 474 | this.app.setting.tabHeadersEl.append(header); 475 | } 476 | this.search.setValue(""); 477 | } 478 | } 479 | detachFromDesktop() { 480 | if (Platform.isDesktop) { 481 | this.app.setting.openTabById(this.app.setting.lastTabId); 482 | } 483 | } 484 | detach() { 485 | this.detachFromDesktop(); 486 | this.detachFromMobile(); 487 | this.searchAppended = false; 488 | } 489 | onChange(v: string) { 490 | if (!v) { 491 | this.detach(); 492 | this.app.keymap.popScope(this.scope); 493 | return; 494 | } 495 | if (!this.searchAppended) { 496 | this.activeIndex = -1; 497 | this.app.keymap.popScope(this.scope); 498 | this.app.keymap.pushScope(this.scope); 499 | if (this.activeSetting) { 500 | this.activeSetting.settingEl.removeClass("active"); 501 | this.activeSetting = null; 502 | } 503 | 504 | if (!Platform.isMobile) { 505 | this.app.setting.activeTab.navEl.removeClass("is-active"); 506 | this.app.setting.tabContentContainer.empty(); 507 | this.app.setting.tabContentContainer.append( 508 | this.settingsResultsContainerEl 509 | ); 510 | } else { 511 | const headers = 512 | this.app.setting.tabHeadersEl.querySelectorAll( 513 | ".vertical-tab-header-group:not(.settings-search-container)" 514 | ); 515 | for (const header of Array.from(headers)) { 516 | this.mobileContainers.push(header); 517 | header.detach(); 518 | } 519 | this.app.setting.tabHeadersEl.append( 520 | this.settingsResultsContainerEl 521 | ); 522 | } 523 | this.searchAppended = true; 524 | } 525 | this.appendResults(this.performFuzzySearch(v)); 526 | } 527 | getMatchText(text: string, result: SearchResult) { 528 | const matchElements: Record = {}; 529 | return createFragment((content) => { 530 | for (let i = 0; i < text.length; i++) { 531 | let match = result.matches.find((m) => m[0] === i); 532 | if (match) { 533 | const index = result.matches.indexOf(match); 534 | if (!matchElements[index]) { 535 | matchElements[index] = createSpan( 536 | "suggestion-highlight" 537 | ); 538 | } 539 | let element = matchElements[index]; 540 | content.appendChild(element); 541 | element.appendText(text.substring(match[0], match[1])); 542 | 543 | i += match[1] - match[0] - 1; 544 | continue; 545 | } 546 | 547 | content.appendText(text[i]); 548 | } 549 | }); 550 | } 551 | appendResults(results: Resource[]) { 552 | this.settingsResultsEl.empty(); 553 | if (results.length) { 554 | const headers: Record = {}; 555 | for (const resource of results) { 556 | if (!(resource.tab in headers)) { 557 | headers[resource.tab] = this.settingsResultsEl.createDiv(); 558 | 559 | new Setting(headers[resource.tab]) 560 | .setHeading() 561 | .setName(resource.name); 562 | } 563 | 564 | const setting = this.getResourceFromCache(resource); 565 | 566 | headers[resource.tab].append(setting.settingEl); 567 | } 568 | /* if ("hotkeys" in headers) { 569 | this.settingsResultsEl.appendChild(headers["hotkeys"]); 570 | } */ 571 | } else { 572 | this.settingsResultsEl.setText("No results found :("); 573 | } 574 | } 575 | 576 | showResult(result: Resource) { 577 | this.search.setValue(""); 578 | const tab = 579 | this.app.setting.settingTabs.find((t) => t.id == result.tab) ?? 580 | this.app.setting.pluginTabs.find((t) => t.id == result.tab); 581 | if (!tab) { 582 | new Notice("There was an issue opening the setting tab."); 583 | return; 584 | } 585 | 586 | this.app.setting.openTabById(tab.id); 587 | this.app.keymap.popScope(this.scope); 588 | this.detach(); 589 | 590 | try { 591 | const names = 592 | tab.containerEl.querySelectorAll(".setting-item-name"); 593 | const el = Array.from(names).find( 594 | (n) => n.textContent == result.text 595 | ); 596 | if (!el) return; 597 | 598 | const setting = el.closest(".setting-item"); 599 | if (!setting) return; 600 | 601 | if (tab.id == "obsidian-style-settings") { 602 | let collapsed = setting.closest(".style-settings-container"); 603 | let previous = collapsed?.previousElementSibling; 604 | 605 | while ( 606 | previous != null && 607 | previous.hasClass("is-collapsed") && 608 | previous.hasClass("style-settings-heading") 609 | ) { 610 | previous.removeClass("is-collapsed"); 611 | collapsed = collapsed.parentElement?.closest( 612 | ".style-settings-container" 613 | ); 614 | previous = collapsed?.previousElementSibling; 615 | } 616 | } 617 | 618 | let details = setting.closest("details"); 619 | while (details) { 620 | details.setAttr("open", "open"); 621 | details = details.parentElement?.closest("details"); 622 | } 623 | 624 | setting.scrollIntoView(true); 625 | 626 | setting.addClass("is-flashing"); 627 | window.setTimeout(() => setting.removeClass("is-flashing"), 3000); 628 | } catch (e) { 629 | console.error(e); 630 | } 631 | } 632 | 633 | performFuzzySearch(input: string) { 634 | const results: Resource[] = [], 635 | hotkeys: Resource[] = []; 636 | for (const resource of this.resources) { 637 | let result = 638 | prepareSimpleSearch(input)(resource.text) ?? 639 | prepareSimpleSearch(input)(resource.desc); 640 | if (result) { 641 | if (resource.tab == "hotkeys") { 642 | hotkeys.push(resource); 643 | } else { 644 | results.push(resource); 645 | } 646 | } 647 | } 648 | this.results = [...results, ...hotkeys]; 649 | return this.results; 650 | } 651 | 652 | onunload() { 653 | this.settingsSearchEl.detach(); 654 | 655 | this.settingsResultsEl.detach(); 656 | this.detach(); 657 | if (this.searchAppended && Platform.isDesktop) 658 | this.app.setting.openTabById(this.app.setting.lastTabId); 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import './assets/main.css'; 2 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | const sveltePreprocess = require("svelte-preprocess"); 2 | 3 | module.exports = { 4 | preprocess: sveltePreprocess(), 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "esnext", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "types": ["node"], 13 | "lib": ["dom", "esnext", "scripthost", "es2015"] 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.12.17" 3 | } --------------------------------------------------------------------------------