Spaces:
Sleeping
Sleeping
Add favicon and image assets for Obsidian help and developer documentation
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +162 -0
- README.md +1 -1
- app.py +177 -0
- docs/obsidian-developer/Assets/command.png +0 -0
- docs/obsidian-developer/Assets/context-menu-positions.png +0 -0
- docs/obsidian-developer/Assets/decorations.svg +173 -0
- docs/obsidian-developer/Assets/default-violet.webp +0 -0
- docs/obsidian-developer/Assets/editor-todays-date.gif +0 -0
- docs/obsidian-developer/Assets/editor-uppercase.gif +0 -0
- docs/obsidian-developer/Assets/example-insert-link.gif +0 -0
- docs/obsidian-developer/Assets/fuzzy-suggestion-modal.png +0 -0
- docs/obsidian-developer/Assets/logo.svg +15 -0
- docs/obsidian-developer/Assets/modal-input.png +0 -0
- docs/obsidian-developer/Assets/obsidian-lockup-docs.svg +1 -0
- docs/obsidian-developer/Assets/settings-headings.png +0 -0
- docs/obsidian-developer/Assets/settings.png +0 -0
- docs/obsidian-developer/Assets/status-bar.png +0 -0
- docs/obsidian-developer/Assets/styles.png +0 -0
- docs/obsidian-developer/Assets/suggest-modal.gif +0 -0
- docs/obsidian-developer/Assets/user-interface.png +0 -0
- docs/obsidian-developer/Assets/viewport.svg +85 -0
- docs/obsidian-developer/Developer policies.md +55 -0
- docs/obsidian-developer/Home.md +35 -0
- docs/obsidian-developer/Plugins/Editor/Communicating with editor extensions.md +53 -0
- docs/obsidian-developer/Plugins/Editor/Decorations.md +226 -0
- docs/obsidian-developer/Plugins/Editor/Editor extensions.md +32 -0
- docs/obsidian-developer/Plugins/Editor/Editor.md +71 -0
- docs/obsidian-developer/Plugins/Editor/Markdown post processing.md +76 -0
- docs/obsidian-developer/Plugins/Editor/State fields.md +95 -0
- docs/obsidian-developer/Plugins/Editor/State management.md +64 -0
- docs/obsidian-developer/Plugins/Editor/View plugins.md +54 -0
- docs/obsidian-developer/Plugins/Editor/Viewport.md +12 -0
- docs/obsidian-developer/Plugins/Events.md +50 -0
- docs/obsidian-developer/Plugins/Getting started/Anatomy of a plugin.md +40 -0
- docs/obsidian-developer/Plugins/Getting started/Build a plugin.md +131 -0
- docs/obsidian-developer/Plugins/Getting started/Development workflow.md +19 -0
- docs/obsidian-developer/Plugins/Getting started/Mobile development.md +73 -0
- docs/obsidian-developer/Plugins/Getting started/Use React in your plugin.md +138 -0
- docs/obsidian-developer/Plugins/Getting started/Use Svelte in your plugin.md +187 -0
- docs/obsidian-developer/Plugins/Releasing/Beta-testing plugins.md +3 -0
- docs/obsidian-developer/Plugins/Releasing/Plugin guidelines.md +301 -0
- docs/obsidian-developer/Plugins/Releasing/Release your plugin with GitHub Actions.md +71 -0
- docs/obsidian-developer/Plugins/Releasing/Submission requirements for plugins.md +43 -0
- docs/obsidian-developer/Plugins/Releasing/Submit your plugin.md +96 -0
- docs/obsidian-developer/Plugins/User interface/About user interface.md +11 -0
- docs/obsidian-developer/Plugins/User interface/Commands.md +121 -0
- docs/obsidian-developer/Plugins/User interface/Context menus.md +80 -0
- docs/obsidian-developer/Plugins/User interface/HTML elements.md +83 -0
- docs/obsidian-developer/Plugins/User interface/Icons.md +72 -0
- docs/obsidian-developer/Plugins/User interface/Modals.md +169 -0
.gitignore
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# poetry
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
102 |
+
#poetry.lock
|
103 |
+
|
104 |
+
# pdm
|
105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
106 |
+
#pdm.lock
|
107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
108 |
+
# in version control.
|
109 |
+
# https://pdm.fming.dev/#use-with-ide
|
110 |
+
.pdm.toml
|
111 |
+
|
112 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
113 |
+
__pypackages__/
|
114 |
+
|
115 |
+
# Celery stuff
|
116 |
+
celerybeat-schedule
|
117 |
+
celerybeat.pid
|
118 |
+
|
119 |
+
# SageMath parsed files
|
120 |
+
*.sage.py
|
121 |
+
|
122 |
+
# Environments
|
123 |
+
.env
|
124 |
+
.venv
|
125 |
+
env/
|
126 |
+
venv/
|
127 |
+
ENV/
|
128 |
+
env.bak/
|
129 |
+
venv.bak/
|
130 |
+
|
131 |
+
# Spyder project settings
|
132 |
+
.spyderproject
|
133 |
+
.spyproject
|
134 |
+
|
135 |
+
# Rope project settings
|
136 |
+
.ropeproject
|
137 |
+
|
138 |
+
# mkdocs documentation
|
139 |
+
/site
|
140 |
+
|
141 |
+
# mypy
|
142 |
+
.mypy_cache/
|
143 |
+
.dmypy.json
|
144 |
+
dmypy.json
|
145 |
+
|
146 |
+
# Pyre type checker
|
147 |
+
.pyre/
|
148 |
+
|
149 |
+
# pytype static type analyzer
|
150 |
+
.pytype/
|
151 |
+
|
152 |
+
# Cython debug symbols
|
153 |
+
cython_debug/
|
154 |
+
|
155 |
+
# PyCharm
|
156 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
157 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
158 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
159 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
160 |
+
#.idea/
|
161 |
+
.cache
|
162 |
+
.vscode
|
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title: Obsidian
|
3 |
emoji: ⚡
|
4 |
colorFrom: gray
|
5 |
colorTo: gray
|
|
|
1 |
---
|
2 |
+
title: Obsidian QA Bot
|
3 |
emoji: ⚡
|
4 |
colorFrom: gray
|
5 |
colorTo: gray
|
app.py
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import gradio as gr
|
3 |
+
|
4 |
+
from langchain_community.document_loaders import TextLoader
|
5 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter, Language
|
6 |
+
|
7 |
+
from langchain.embeddings import CacheBackedEmbeddings
|
8 |
+
from langchain.storage import LocalFileStore
|
9 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
10 |
+
from langchain_community.vectorstores import FAISS
|
11 |
+
|
12 |
+
from langchain_community.retrievers import BM25Retriever
|
13 |
+
from langchain.retrievers import EnsembleRetriever
|
14 |
+
|
15 |
+
from langchain_cohere import CohereRerank
|
16 |
+
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
|
17 |
+
|
18 |
+
from langchain_core.prompts import PromptTemplate
|
19 |
+
|
20 |
+
from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
21 |
+
from langchain_core.callbacks.manager import CallbackManager
|
22 |
+
from langchain_core.runnables import ConfigurableField
|
23 |
+
from langchain.callbacks.base import BaseCallbackHandler
|
24 |
+
from langchain_core.output_parsers import StrOutputParser
|
25 |
+
from langchain_core.runnables import RunnablePassthrough
|
26 |
+
from langchain_groq import ChatGroq
|
27 |
+
from langchain_community.llms import HuggingFaceHub
|
28 |
+
from langchain_google_genai import GoogleGenerativeAI
|
29 |
+
|
30 |
+
|
31 |
+
directories = ["./docs/obsidian-help", "./docs/obsidian-developer"]
|
32 |
+
|
33 |
+
|
34 |
+
# 1. 문서 로더를 사용하여 모든 .md 파일을 로드합니다.
|
35 |
+
md_documents = []
|
36 |
+
for directory in directories:
|
37 |
+
# os.walk를 사용하여 root_dir부터 시작하는 모든 디렉토리를 순회합니다.
|
38 |
+
for dirpath, dirnames, filenames in os.walk(directory):
|
39 |
+
# 각 디렉토리에서 파일 목록을 확인합니다.
|
40 |
+
for file in filenames:
|
41 |
+
# 파일 확장자가 .md인지 확인하고, 경로 내 '*venv/' 문자열이 포함되지 않는지도 체크합니다.
|
42 |
+
if (file.endswith(".md")) and "*venv/" not in dirpath:
|
43 |
+
try:
|
44 |
+
# TextLoader를 사용하여 파일의 전체 경로를 지정하고 문서를 로드합니다.
|
45 |
+
loader = TextLoader(os.path.join(dirpath, file), encoding="utf-8")
|
46 |
+
# 로드한 문서를 분할하여 documents 리스트에 추가합니다.
|
47 |
+
md_documents.extend(loader.load())
|
48 |
+
except Exception:
|
49 |
+
# 파일 로드 중 오류가 발생하면 이를 무시하고 계속 진행합니다.
|
50 |
+
pass
|
51 |
+
|
52 |
+
|
53 |
+
# 2. 청크 분할기를 생성합니다.
|
54 |
+
# 청크 크기는 2000, 청크간 겹치는 부분은 200 문자로 설정합니다.
|
55 |
+
md_splitter = RecursiveCharacterTextSplitter.from_language(
|
56 |
+
language=Language.MARKDOWN,
|
57 |
+
chunk_size=2000,
|
58 |
+
chunk_overlap=200,
|
59 |
+
)
|
60 |
+
md_docs = md_splitter.split_documents(md_documents)
|
61 |
+
|
62 |
+
|
63 |
+
# 3. 임베딩 모델을 사용하여 문서의 임베딩을 계산합니다.
|
64 |
+
# 허깅페이스 임베딩 모델 인스턴스를 생성합니다. 모델명으로 "BAAI/bge-m3 "을 사용합니다.
|
65 |
+
model_name = "BAAI/bge-m3"
|
66 |
+
model_kwargs = {"device": "mps"}
|
67 |
+
encode_kwargs = {"normalize_embeddings": False}
|
68 |
+
embeddings = HuggingFaceEmbeddings(
|
69 |
+
model_name=model_name,
|
70 |
+
model_kwargs=model_kwargs,
|
71 |
+
encode_kwargs=encode_kwargs,
|
72 |
+
)
|
73 |
+
|
74 |
+
# CacheBackedEmbeddings를 사용하여 임베딩 계산 결과를 캐시합니다.
|
75 |
+
store = LocalFileStore("./.cache/")
|
76 |
+
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
|
77 |
+
embeddings,
|
78 |
+
store,
|
79 |
+
namespace=embeddings.model_name,
|
80 |
+
)
|
81 |
+
|
82 |
+
# 4. FAISS 벡터 데이터베이스 인덱스를 생성하고 저장합니다.
|
83 |
+
FAISS_DB_INDEX = "db_index"
|
84 |
+
|
85 |
+
if os.path.exists(FAISS_DB_INDEX):
|
86 |
+
# 저장된 데이터베이스 인덱스가 이미 존재하는 경우, 해당 인덱스를 로드합니다.
|
87 |
+
db = FAISS.load_local(
|
88 |
+
FAISS_DB_INDEX, # 로드할 FAISS 인덱스의 디렉토리 이름
|
89 |
+
cached_embeddings, # 임베딩 정보를 제공
|
90 |
+
allow_dangerous_deserialization=True, # 역직렬화를 허용하는 옵션
|
91 |
+
)
|
92 |
+
else:
|
93 |
+
# combined_documents 문서들과 cached_embeddings 임베딩을 사용하여
|
94 |
+
# FAISS 데이터베이스 인스턴스를 생성합니다.
|
95 |
+
db = FAISS.from_documents(md_docs, cached_embeddings)
|
96 |
+
# 생성된 데이터베이스 인스턴스를 지정한 폴더에 로컬로 저장합니다.
|
97 |
+
db.save_local(folder_path=FAISS_DB_INDEX)
|
98 |
+
|
99 |
+
|
100 |
+
# 5. Retrieval를 생성합니다.
|
101 |
+
faiss_retriever = db.as_retriever(search_type="mmr", search_kwargs={"k": 10})
|
102 |
+
|
103 |
+
# 문서 컬렉션을 사용하여 BM25 검색 모델 인스턴스를 생성합니다.
|
104 |
+
bm25_retriever = BM25Retriever.from_documents(md_docs) # 초기화에 사용할 문서 컬렉션
|
105 |
+
bm25_retriever.k = 10 # 검색 시 최대 10개의 결과를 반환하도록 합니다.
|
106 |
+
|
107 |
+
# EnsembleRetriever 인스턴스를 생성합니다.
|
108 |
+
ensemble_retriever = EnsembleRetriever(
|
109 |
+
retrievers=[bm25_retriever, faiss_retriever], # 사용할 검색 모델의 리스트
|
110 |
+
weights=[0.6, 0.4], # 각 검색 모델의 결과에 적용할 가중치
|
111 |
+
search_type="mmr", # 검색 결과의 다양성을 증진시키는 MMR 방식을 사��
|
112 |
+
)
|
113 |
+
|
114 |
+
# 6. CohereRerank 모델을 사용하여 재정렬을 수행합니다.
|
115 |
+
compressor = CohereRerank(model="rerank-multilingual-v3.0")
|
116 |
+
compression_retriever = ContextualCompressionRetriever(
|
117 |
+
base_compressor=compressor,
|
118 |
+
base_retriever=ensemble_retriever,
|
119 |
+
)
|
120 |
+
|
121 |
+
# 7. Prompt를 생성합니다.
|
122 |
+
prompt = PromptTemplate.from_template(
|
123 |
+
"""당신은 20년 경력의 옵시디언 노트앱 및 플러그인 개발 전문가로, 옵시디언 노트앱 사용법, 플러그인 및 테마 개발에 대한 깊은 지식을 가지고 있습니다. 당신의 주된 임무는 제공된 문서를 바탕으로 질문에 최대한 정확하고 상세하게 답변하는 것입니다.
|
124 |
+
문서에는 옵시디언 노트앱의 기본 사용법, 고급 기능, 플러그인 개발 방법, 테마 개발 가이드 등 옵시디언 노트앱을 깊이 있게 사용하고 확장하는 데 필요한 정보가 포함되어 있습니다.
|
125 |
+
귀하의 답변은 다음 지침에 따라야 합니다:
|
126 |
+
1. 모든 답변은 명확하고 이해하기 쉬운 한국어로 제공되어야 합니다.
|
127 |
+
2. 답변은 문서의 내용을 기반으로 해야 하며, 가능한 한 구체적인 정보를 포함해야 합니다.
|
128 |
+
3. 문서 내에서 직접적인 답변을 찾을 수 없는 경우, "문서에는 해당 질문에 대한 구체적인 답변이 없습니다."라고 명시해 주세요.
|
129 |
+
4. 가능한 경우, 답변과 관련된 문서의 구체적인 부분(예: 섹션 이름, 페이지 번호 등)을 출처로서 명시해 주세요.
|
130 |
+
5. 질문에 대한 답변이 문서에 부분적으로만 포함되어 있는 경우, 가능한 한 많은 정보를 종합하여 답변해 주세요. 또한, 추가적인 연구나 참고자료가 필요할 수 있음을 언급해 주세요.
|
131 |
+
|
132 |
+
#참고문서:
|
133 |
+
{context}
|
134 |
+
|
135 |
+
#질문:
|
136 |
+
{question}
|
137 |
+
|
138 |
+
#답변:
|
139 |
+
|
140 |
+
출처:
|
141 |
+
- source1
|
142 |
+
- source2
|
143 |
+
- ...
|
144 |
+
"""
|
145 |
+
)
|
146 |
+
|
147 |
+
|
148 |
+
# 7. chain를 생성합니다.
|
149 |
+
llm = ChatGroq(
|
150 |
+
model_name="llama3-70b-8192",
|
151 |
+
temperature=0,
|
152 |
+
).configurable_alternatives(
|
153 |
+
ConfigurableField(id="llm"),
|
154 |
+
default_key="llama3",
|
155 |
+
gemini=GoogleGenerativeAI(
|
156 |
+
model="gemini-pro",
|
157 |
+
temperature=0,
|
158 |
+
),
|
159 |
+
)
|
160 |
+
|
161 |
+
rag_chain = (
|
162 |
+
{"context": compression_retriever, "question": RunnablePassthrough()}
|
163 |
+
| prompt
|
164 |
+
| llm
|
165 |
+
| StrOutputParser()
|
166 |
+
)
|
167 |
+
|
168 |
+
# # 8. chain를 실행합니다.
|
169 |
+
def predict(message, history=None):
|
170 |
+
answer = rag_chain.invoke(message)
|
171 |
+
return answer
|
172 |
+
|
173 |
+
gr.ChatInterface(
|
174 |
+
predict,
|
175 |
+
title="옵시디언 노트앱 및 플러그인 개발에 대해서 물어보세요!",
|
176 |
+
description="안녕하세요!\n저는 옵시디언 노트앱과 플러그인 개발에 대한 인공지능 QA봇입니다. 옵시디언 노트앱의 사용법, 고급 기능, 플러그인 및 테마 개발에 대해 깊은 지식을 가지고 있어요. 문서 작업, 정보 정리 또는 개발에 관한 도움이 필요하시면 언제든지 질문해주세요!",
|
177 |
+
).launch()
|
docs/obsidian-developer/Assets/command.png
ADDED
docs/obsidian-developer/Assets/context-menu-positions.png
ADDED
docs/obsidian-developer/Assets/decorations.svg
ADDED
docs/obsidian-developer/Assets/default-violet.webp
ADDED
docs/obsidian-developer/Assets/editor-todays-date.gif
ADDED
docs/obsidian-developer/Assets/editor-uppercase.gif
ADDED
docs/obsidian-developer/Assets/example-insert-link.gif
ADDED
docs/obsidian-developer/Assets/fuzzy-suggestion-modal.png
ADDED
docs/obsidian-developer/Assets/logo.svg
ADDED
docs/obsidian-developer/Assets/modal-input.png
ADDED
docs/obsidian-developer/Assets/obsidian-lockup-docs.svg
ADDED
docs/obsidian-developer/Assets/settings-headings.png
ADDED
docs/obsidian-developer/Assets/settings.png
ADDED
docs/obsidian-developer/Assets/status-bar.png
ADDED
docs/obsidian-developer/Assets/styles.png
ADDED
docs/obsidian-developer/Assets/suggest-modal.gif
ADDED
docs/obsidian-developer/Assets/user-interface.png
ADDED
docs/obsidian-developer/Assets/viewport.svg
ADDED
docs/obsidian-developer/Developer policies.md
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Our goal for community plugins and themes is to make it easy for users to safely modify and expand the capabilities of Obsidian, while prioritizing private and offline usage of the app.
|
2 |
+
|
3 |
+
All community plugins and themes added to the Obsidian directory must respect the following policies. Every plugin and theme is individually vetted before being included in the directory. Plugins and themes that don't follow these policies will be removed from the directory.
|
4 |
+
|
5 |
+
## Policies
|
6 |
+
|
7 |
+
### Not allowed
|
8 |
+
|
9 |
+
Plugins and themes must not:
|
10 |
+
|
11 |
+
- Obfuscate code to hide its purpose.
|
12 |
+
- Insert dynamic ads that are loaded over the internet.
|
13 |
+
- Insert static ads outside a plugin’s own interface.
|
14 |
+
- Include client-side telemetry.
|
15 |
+
- Themes may not load assets from the network. To bundle an asset, see [[Embed fonts and images in your theme|this guide]].
|
16 |
+
|
17 |
+
### Disclosures
|
18 |
+
|
19 |
+
The following are only allowed if clearly indicated in your README:
|
20 |
+
|
21 |
+
- Payment is required for full access.
|
22 |
+
- An account is required for full access.
|
23 |
+
- Network use. Clearly explain which remote services are used and why they're needed.
|
24 |
+
- Accessing files outside of Obsidian vaults. Clearly explain why this is needed.
|
25 |
+
- Static ads such as banners and pop-up messages within the plugin's own interface.
|
26 |
+
- Server-side telemetry. Link to a privacy policy that explains how the data is handled must be included.
|
27 |
+
- Close sourced code. This will be handled on a case by case basis.
|
28 |
+
|
29 |
+
### Copyright and licensing
|
30 |
+
|
31 |
+
All community plugins and themes must follow these requirements:
|
32 |
+
|
33 |
+
- Include a [LICENSE file](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository) and clearly indicate the license of your plugin or theme.
|
34 |
+
- Comply with the original licenses of any code your plugin or theme makes use of, including attribution in the README if required.
|
35 |
+
- Respect Obsidian's trademark policy. Don't use the "Obsidian" trademark in a way that could confuse users into thinking your plugin or theme is a first-party creation.
|
36 |
+
|
37 |
+
## Reporting violations
|
38 |
+
|
39 |
+
If you encounter a plugin or theme that violates the policies above, please let the developer know by opening a GitHub issue in their repository. Kindly check existing issues to see if it’s already reported.
|
40 |
+
|
41 |
+
If the developer doesn’t response after 7 days, [contact the Obsidian team](https://help.obsidian.md/Help+and+support#Report+a+security+issue). For serious violations, you can contact our team immediately.
|
42 |
+
|
43 |
+
## Removing plugins and themes
|
44 |
+
|
45 |
+
In case of a policy violation, we may attempt to contact the developer and provide a reasonable timeframe for them to resolve the problem.
|
46 |
+
|
47 |
+
If the problem isn't resolved by then, we'll remove plugins or themes from our directory.
|
48 |
+
|
49 |
+
We may immediately remove a plugin or theme if:
|
50 |
+
|
51 |
+
- The plugin or theme appears to be malicious.
|
52 |
+
- The developer is uncooperative.
|
53 |
+
- This is a repeated violation.
|
54 |
+
|
55 |
+
In addition, we may also remove plugins or themes that have become unmaintained or severely broken.
|
docs/obsidian-developer/Home.md
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
cssClass: hide-title
|
3 |
+
---
|
4 |
+
# Obsidian Developer Docs
|
5 |
+
|
6 |
+
Welcome to the official Obsidian Developer Documentation, where you can learn how to build plugins and themes for [Obsidian](https://obsidian.md/). For tips on how to use Obsidian, visit [the official Help site](https://help.obsidian.md/).
|
7 |
+
|
8 |
+
## Plugins
|
9 |
+
|
10 |
+
Build plugins to extend the existing functionality in Obsidian using TypeScript.
|
11 |
+
|
12 |
+
- [[Build a plugin|Build your first plugin]]
|
13 |
+
- [[Submit your plugin]]
|
14 |
+
|
15 |
+
## Themes
|
16 |
+
|
17 |
+
Design beautiful themes and snippets for Obsidian using CSS.
|
18 |
+
|
19 |
+
- [[Build a theme|Build your first theme]]
|
20 |
+
- [[Submit your theme]]
|
21 |
+
- [[CSS variables]]
|
22 |
+
|
23 |
+
## Join the developer community
|
24 |
+
|
25 |
+
If you get stuck, or if you're looking for feedback, [join the community](https://obsidian.md/community).
|
26 |
+
|
27 |
+
- `#plugin-dev` and `#theme-dev` channels on Discord.
|
28 |
+
- [Developers & API](https://forum.obsidian.md/c/developers-api/14) and [Share & showcase](https://forum.obsidian.md/c/share-showcase/9) on the forum.
|
29 |
+
|
30 |
+
## Contributing
|
31 |
+
|
32 |
+
If you see any errors or room for improvement on this site, or want to submit a PR, feel free to open an issue on [our GitHub repository](https://github.com/obsidianmd/obsidian-developer-docs).
|
33 |
+
Additional details are available on our [readme](https://github.com/obsidianmd/obsidian-developer-docs#readme).
|
34 |
+
|
35 |
+
Thank you in advance for contributing!
|
docs/obsidian-developer/Plugins/Editor/Communicating with editor extensions.md
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Once you've built your editor extension, you might want to communicate with it from outside the editor. For example, through a [[Commands|command]], or a [[Ribbon actions|ribbon action]].
|
2 |
+
|
3 |
+
You can access the CodeMirror 6 editor from a [[MarkdownView|MarkdownView]]. However, since the Obsidian API doesn't actually expose the editor, you need to tell TypeScript to trust that it's there, using `@ts-expect-error`.
|
4 |
+
|
5 |
+
```ts
|
6 |
+
import { EditorView } from "@codemirror/view";
|
7 |
+
|
8 |
+
// @ts-expect-error, not typed
|
9 |
+
const editorView = view.editor.cm as EditorView;
|
10 |
+
```
|
11 |
+
|
12 |
+
## View plugin
|
13 |
+
|
14 |
+
You can access the [[View plugins|view plugin]] instance from the `EditorView.plugin()` method.
|
15 |
+
|
16 |
+
```ts
|
17 |
+
this.addCommand({
|
18 |
+
id: "example-editor-command",
|
19 |
+
name: "Example editor command",
|
20 |
+
editorCallback: (editor, view) => {
|
21 |
+
// @ts-expect-error, not typed
|
22 |
+
const editorView = view.editor.cm as EditorView;
|
23 |
+
|
24 |
+
const plugin = editorView.plugin(examplePlugin);
|
25 |
+
|
26 |
+
if (plugin) {
|
27 |
+
plugin.addPointerToSelection(editorView);
|
28 |
+
}
|
29 |
+
},
|
30 |
+
});
|
31 |
+
```
|
32 |
+
|
33 |
+
## State field
|
34 |
+
|
35 |
+
You can dispatch changes and [[State fields#Dispatching state effects|dispatch state effects]] directly on the editor view.
|
36 |
+
|
37 |
+
```ts
|
38 |
+
this.addCommand({
|
39 |
+
id: "example-editor-command",
|
40 |
+
name: "Example editor command",
|
41 |
+
editorCallback: (editor, view) => {
|
42 |
+
// @ts-expect-error, not typed
|
43 |
+
const editorView = view.editor.cm as EditorView;
|
44 |
+
|
45 |
+
editorView.dispatch({
|
46 |
+
effects: [
|
47 |
+
// ...
|
48 |
+
],
|
49 |
+
});
|
50 |
+
},
|
51 |
+
});
|
52 |
+
```
|
53 |
+
|
docs/obsidian-developer/Plugins/Editor/Decorations.md
ADDED
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Decorations let you control how to draw or style content in [[Editor extensions|editor extensions]]. If you intend to change the look and feel by adding, replacing, or styling elements in the editor, you most likely need to use decorations.
|
2 |
+
|
3 |
+
By the end of this page, you'll be able to:
|
4 |
+
|
5 |
+
- Understand how to use decorations to change the editor appearance.
|
6 |
+
- Understand the difference between providing decoration using state fields and view plugins.
|
7 |
+
|
8 |
+
> [!note]
|
9 |
+
> This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more detailed information on state fields, refer to [Decorating the Document](https://codemirror.net/docs/guide/#decorating-the-document).
|
10 |
+
|
11 |
+
## Prerequisites
|
12 |
+
|
13 |
+
- Basic understanding of [[State fields]].
|
14 |
+
- Basic understanding of [[View plugins]].
|
15 |
+
|
16 |
+
## Overview
|
17 |
+
|
18 |
+
Without decorations, the document would render as plain text. Not very interesting at all. Using decorations, you can change how to display the document, for example by highlighting text or adding custom HTML elements.
|
19 |
+
|
20 |
+
You can use the following types of decorations:
|
21 |
+
|
22 |
+
- [Mark decorations](https://codemirror.net/docs/ref/#view.Decoration%5Emark) style existing elements.
|
23 |
+
- [Widget decorations](https://codemirror.net/docs/ref/#view.Decoration%5Ewidget) insert elements in the document.
|
24 |
+
- [Replace decorations](https://codemirror.net/docs/ref/#view.Decoration%5Ereplace) hide or replace part of the document with another element.
|
25 |
+
- [Line decorations](https://codemirror.net/docs/ref/#view.Decoration%5Eline) add styling to the lines, rather than the document itself.
|
26 |
+
|
27 |
+
To use decorations, you need to create them inside an editor extension and have the extension _provide_ them to the editor. You can provide decorations to the editor in two ways, either _directly_ using [[State fields|state fields]] or _indirectly_ using [[View plugins|view plugins]].
|
28 |
+
|
29 |
+
## Should I use a view plugin or a state field?
|
30 |
+
|
31 |
+
Both view plugins and state fields can provide decorations to the editor, but they have some differences.
|
32 |
+
|
33 |
+
- Use a view plugin if you can determine the decoration based on what's inside the [[Viewport]].
|
34 |
+
- Use a state field if you need to manage decorations outside of the viewport.
|
35 |
+
- Use a state field if you want to make changes that could change the content of the viewport, for example by adding line breaks.
|
36 |
+
|
37 |
+
If you can implement your extension using either approach, then the view plugin generally results in better performance. For example, imagine that you want to implement an editor extension that checks the spelling of a document.
|
38 |
+
|
39 |
+
One way would be to pass the entire document to an external spell checker which then returns a list of spelling errors. In this case, you'd need to map each error to a decoration and use a state field to manage decorations regardless of what's in the viewport at the moment.
|
40 |
+
|
41 |
+
Another way would be to only spellcheck what's visible in the viewport. The extension would need to continuously run a spell check as the user scrolls through the document, but you'd be able to spell check documents with millions of lines of text.
|
42 |
+
|
43 |
+
![State field vs. view plugin](decorations.svg)
|
44 |
+
|
45 |
+
## Providing decorations
|
46 |
+
|
47 |
+
Imagine that you want to build an editor extension that replaces the bullet list item with an emoji. You can accomplish this with either a view plugin or a state field, with some differences. In this section, you'll see how to implement it with both types of extensions.
|
48 |
+
|
49 |
+
Both implementations share the same core logic:
|
50 |
+
|
51 |
+
1. Use [syntaxTree](https://codemirror.net/docs/ref/#language.syntaxTree) to find list items.
|
52 |
+
2. For every list item, replace leading hyphens, `-`, with a _widget_.
|
53 |
+
|
54 |
+
### Widgets
|
55 |
+
|
56 |
+
Widgets are custom HTML elements that you can add to the editor. You can either insert a widget at a specific position in the document, or replace a piece of content with a widget.
|
57 |
+
|
58 |
+
The following example defines a widget that returns an HTML element, `<span>👉</span>`. You'll use this widget later on.
|
59 |
+
|
60 |
+
```ts
|
61 |
+
import { EditorView, WidgetType } from "@codemirror/view";
|
62 |
+
|
63 |
+
export class EmojiWidget extends WidgetType {
|
64 |
+
toDOM(view: EditorView): HTMLElement {
|
65 |
+
const div = document.createElement("span");
|
66 |
+
|
67 |
+
div.innerText = "👉";
|
68 |
+
|
69 |
+
return div;
|
70 |
+
}
|
71 |
+
}
|
72 |
+
```
|
73 |
+
|
74 |
+
To replace a range of content in your document with the emoji widget, use the [replace decoration](https://codemirror.net/docs/ref/#view.Decoration%5Ereplace).
|
75 |
+
|
76 |
+
```ts
|
77 |
+
const decoration = Decoration.replace({
|
78 |
+
widget: new EmojiWidget()
|
79 |
+
});
|
80 |
+
```
|
81 |
+
|
82 |
+
### State fields
|
83 |
+
|
84 |
+
To provide decorations from a state field:
|
85 |
+
|
86 |
+
1. [[State fields#Defining a state field|Define a state field]] with a `DecorationSet` type.
|
87 |
+
2. Add the `provide` property to the state field.
|
88 |
+
|
89 |
+
```ts
|
90 |
+
provide(field: StateField<DecorationSet>): Extension {
|
91 |
+
return EditorView.decorations.from(field);
|
92 |
+
},
|
93 |
+
```
|
94 |
+
|
95 |
+
```ts
|
96 |
+
import { syntaxTree } from "@codemirror/language";
|
97 |
+
import {
|
98 |
+
Extension,
|
99 |
+
RangeSetBuilder,
|
100 |
+
StateField,
|
101 |
+
Transaction,
|
102 |
+
} from "@codemirror/state";
|
103 |
+
import {
|
104 |
+
Decoration,
|
105 |
+
DecorationSet,
|
106 |
+
EditorView,
|
107 |
+
WidgetType,
|
108 |
+
} from "@codemirror/view";
|
109 |
+
import { EmojiWidget } from "emoji";
|
110 |
+
|
111 |
+
export const emojiListField = StateField.define<DecorationSet>({
|
112 |
+
create(state): DecorationSet {
|
113 |
+
return Decoration.none;
|
114 |
+
},
|
115 |
+
update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
|
116 |
+
const builder = new RangeSetBuilder<Decoration>();
|
117 |
+
|
118 |
+
syntaxTree(transaction.state).iterate({
|
119 |
+
enter(node) {
|
120 |
+
if (node.type.name.startsWith("list")) {
|
121 |
+
// Position of the '-' or the '*'.
|
122 |
+
const listCharFrom = node.from - 2;
|
123 |
+
|
124 |
+
builder.add(
|
125 |
+
listCharFrom,
|
126 |
+
listCharFrom + 1,
|
127 |
+
Decoration.replace({
|
128 |
+
widget: new EmojiWidget(),
|
129 |
+
})
|
130 |
+
);
|
131 |
+
}
|
132 |
+
},
|
133 |
+
});
|
134 |
+
|
135 |
+
return builder.finish();
|
136 |
+
},
|
137 |
+
provide(field: StateField<DecorationSet>): Extension {
|
138 |
+
return EditorView.decorations.from(field);
|
139 |
+
},
|
140 |
+
});
|
141 |
+
```
|
142 |
+
|
143 |
+
### View plugins
|
144 |
+
|
145 |
+
To manage your decorations using a view plugin:
|
146 |
+
|
147 |
+
1. [[View plugins#Creating a view plugin|Create a view plugin]].
|
148 |
+
2. Add a `DecorationSet` member property to your plugin.
|
149 |
+
3. Initialize the decorations in the `constructor()`.
|
150 |
+
4. Rebuild decorations in `update()`.
|
151 |
+
|
152 |
+
Not all updates are reasons to rebuild your decorations. The following example only rebuilds decorations whenever the underlying document or the viewport changes.
|
153 |
+
|
154 |
+
```ts
|
155 |
+
import { syntaxTree } from "@codemirror/language";
|
156 |
+
import { RangeSetBuilder } from "@codemirror/state";
|
157 |
+
import {
|
158 |
+
Decoration,
|
159 |
+
DecorationSet,
|
160 |
+
EditorView,
|
161 |
+
PluginSpec,
|
162 |
+
PluginValue,
|
163 |
+
ViewPlugin,
|
164 |
+
ViewUpdate,
|
165 |
+
WidgetType,
|
166 |
+
} from "@codemirror/view";
|
167 |
+
import { EmojiWidget } from "emoji";
|
168 |
+
|
169 |
+
class EmojiListPlugin implements PluginValue {
|
170 |
+
decorations: DecorationSet;
|
171 |
+
|
172 |
+
constructor(view: EditorView) {
|
173 |
+
this.decorations = this.buildDecorations(view);
|
174 |
+
}
|
175 |
+
|
176 |
+
update(update: ViewUpdate) {
|
177 |
+
if (update.docChanged || update.viewportChanged) {
|
178 |
+
this.decorations = this.buildDecorations(update.view);
|
179 |
+
}
|
180 |
+
}
|
181 |
+
|
182 |
+
destroy() {}
|
183 |
+
|
184 |
+
buildDecorations(view: EditorView): DecorationSet {
|
185 |
+
const builder = new RangeSetBuilder<Decoration>();
|
186 |
+
|
187 |
+
for (let { from, to } of view.visibleRanges) {
|
188 |
+
syntaxTree(view.state).iterate({
|
189 |
+
from,
|
190 |
+
to,
|
191 |
+
enter(node) {
|
192 |
+
if (node.type.name.startsWith("list")) {
|
193 |
+
// Position of the '-' or the '*'.
|
194 |
+
const listCharFrom = node.from - 2;
|
195 |
+
|
196 |
+
builder.add(
|
197 |
+
listCharFrom,
|
198 |
+
listCharFrom + 1,
|
199 |
+
Decoration.replace({
|
200 |
+
widget: new EmojiWidget(),
|
201 |
+
})
|
202 |
+
);
|
203 |
+
}
|
204 |
+
},
|
205 |
+
});
|
206 |
+
}
|
207 |
+
|
208 |
+
return builder.finish();
|
209 |
+
}
|
210 |
+
}
|
211 |
+
|
212 |
+
const pluginSpec: PluginSpec<EmojiListPlugin> = {
|
213 |
+
decorations: (value: EmojiListPlugin) => value.decorations,
|
214 |
+
};
|
215 |
+
|
216 |
+
export const emojiListPlugin = ViewPlugin.fromClass(
|
217 |
+
EmojiListPlugin,
|
218 |
+
pluginSpec
|
219 |
+
);
|
220 |
+
```
|
221 |
+
|
222 |
+
`buildDecorations()` is a helper method that builds a complete set of decorations based on the editor view.
|
223 |
+
|
224 |
+
Notice the second argument to the `ViewPlugin.fromClass()` function. The `decorations` property in the `PluginSpec` specifies how the view plugin provides the decorations to the editor.
|
225 |
+
|
226 |
+
Since the view plugin knows what's visible to the user, you can use `view.visibleRanges` to limit what parts of the syntax tree to visit.
|
docs/obsidian-developer/Plugins/Editor/Editor extensions.md
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
alias: editor extension
|
3 |
+
---
|
4 |
+
|
5 |
+
Editor extensions let you customize the experience of editing notes in Obsidian. This page explains what editor extensions are, and when to use them.
|
6 |
+
|
7 |
+
Obsidian uses CodeMirror 6 (CM6) to power the Markdown editor. Just like Obsidian, CM6 has plugins of its own, called _extensions_. In other words, an Obsidian _editor extension_ is the same thing as a _CodeMirror 6 extension_.
|
8 |
+
|
9 |
+
The API for building editor extensions is a bit unconventional and requires that you have a basic understanding of its architecture before you get started. This section aims to give you enough context and examples for you to get started. If you want to learn more about building editor extensions, refer to the [CodeMirror 6 documentation](https://codemirror.net/docs/).
|
10 |
+
|
11 |
+
## Do I need an editor extension?
|
12 |
+
|
13 |
+
Building editor extensions can be challenging, so before you start building one, consider whether you really need it.
|
14 |
+
|
15 |
+
- If you want to change how to convert Markdown to HTML in the Reading view, consider building a [[Markdown post processing|Markdown post processor]].
|
16 |
+
- If you want to change how the document looks and feels in Live Preview, you need to build an editor extension.
|
17 |
+
|
18 |
+
## Registering editor extensions
|
19 |
+
|
20 |
+
CodeMirror 6 (CM6) is a powerful engine for editing code using web technologies. At its core, the editor itself has a minimal set of features. Any features you'd expect from a modern editor are available as _extensions_ that you can pick and choose. While Obsidian comes with many of these extensions out-of-the-box, you can also register your own.
|
21 |
+
|
22 |
+
To register an editor extension, use [[registerEditorExtension|registerEditorExtension()]] in the `onload` method of your Obsidian plugin:
|
23 |
+
|
24 |
+
```ts
|
25 |
+
onload() {
|
26 |
+
this.registerEditorExtension([examplePlugin, exampleField]);
|
27 |
+
}
|
28 |
+
```
|
29 |
+
|
30 |
+
While CM6 supports several types of extensions, two of the most common ones are [[View plugins]] and [[State fields]].
|
31 |
+
<DocCardList items={useCurrentSidebarCategory().items}/>
|
32 |
+
|
docs/obsidian-developer/Plugins/Editor/Editor.md
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The [[Reference/TypeScript API/Editor/Editor|Editor]] class exposes operations for reading and manipulating an active Markdown document in edit mode.
|
2 |
+
|
3 |
+
If you want to access the editor in a command, use the [[Commands#Editor commands|editorCallback]].
|
4 |
+
|
5 |
+
If you want to use the editor elsewhere, you can access it from the active view:
|
6 |
+
|
7 |
+
```ts
|
8 |
+
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
9 |
+
|
10 |
+
// Make sure the user is editing a Markdown file.
|
11 |
+
if (view) {
|
12 |
+
const cursor = view.editor.getCursor();
|
13 |
+
|
14 |
+
// ...
|
15 |
+
}
|
16 |
+
```
|
17 |
+
|
18 |
+
> [!note]
|
19 |
+
> Obsidian uses [CodeMirror](https://codemirror.net/) (CM) as the underlying text editor, and exposes the CodeMirror editor as part of the API. `Editor` serves as an abstraction to bridge features between CM6 and CM5 (legacy editor, only available on desktop). By using `Editor` instead of directly accessing the CodeMirror instance, you ensure that your plugin works on both platforms.
|
20 |
+
|
21 |
+
## Insert text at cursor position
|
22 |
+
|
23 |
+
The [[replaceRange|replaceRange()]] method replaces the text between two cursor positions. If you only give it one position, it inserts the new text between that position and the next.
|
24 |
+
|
25 |
+
The following command inserts today's date at the cursor position:
|
26 |
+
|
27 |
+
```ts
|
28 |
+
import { Editor, moment, Plugin } from "obsidian";
|
29 |
+
|
30 |
+
export default class ExamplePlugin extends Plugin {
|
31 |
+
async onload() {
|
32 |
+
this.addCommand({
|
33 |
+
id: "insert-todays-date",
|
34 |
+
name: "Insert today's date",
|
35 |
+
editorCallback: (editor: Editor) => {
|
36 |
+
editor.replaceRange(
|
37 |
+
moment().format("YYYY-MM-DD"),
|
38 |
+
editor.getCursor()
|
39 |
+
);
|
40 |
+
},
|
41 |
+
});
|
42 |
+
}
|
43 |
+
}
|
44 |
+
```
|
45 |
+
|
46 |
+
![[editor-todays-date.gif]]
|
47 |
+
|
48 |
+
## Replace current selection
|
49 |
+
|
50 |
+
If you want to modify the selected text, use [[replaceRange|replaceSelection()]] to replace the current selection with a new text.
|
51 |
+
|
52 |
+
The following command reads the current selection and converts it to uppercase:
|
53 |
+
|
54 |
+
```ts
|
55 |
+
import { Editor, Plugin } from "obsidian";
|
56 |
+
|
57 |
+
export default class ExamplePlugin extends Plugin {
|
58 |
+
async onload() {
|
59 |
+
this.addCommand({
|
60 |
+
id: "convert-to-uppercase",
|
61 |
+
name: "Convert to uppercase",
|
62 |
+
editorCallback: (editor: Editor) => {
|
63 |
+
const selection = editor.getSelection();
|
64 |
+
editor.replaceSelection(selection.toUpperCase());
|
65 |
+
},
|
66 |
+
});
|
67 |
+
}
|
68 |
+
}
|
69 |
+
```
|
70 |
+
|
71 |
+
![[editor-uppercase.gif]]
|
docs/obsidian-developer/Plugins/Editor/Markdown post processing.md
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
If you want to change how a Markdown document is rendered in Reading view, you can add your own _Markdown post processor_. As indicated by the name, the post processor runs _after_ the Markdown has been processed into HTML. It lets you add, remove, or replace [[HTML elements]] to the rendered document.
|
2 |
+
|
3 |
+
The following example looks for any code block that contains a text between two colons, `:`, and replaces it with an appropriate emoji:
|
4 |
+
|
5 |
+
```ts
|
6 |
+
import { Plugin } from "obsidian";
|
7 |
+
|
8 |
+
const ALL_EMOJIS: Record<string, string> = {
|
9 |
+
":+1:": "👍",
|
10 |
+
":sunglasses:": "😎",
|
11 |
+
":smile:": "😄",
|
12 |
+
};
|
13 |
+
|
14 |
+
export default class ExamplePlugin extends Plugin {
|
15 |
+
async onload() {
|
16 |
+
this.registerMarkdownPostProcessor((element, context) => {
|
17 |
+
const codeblocks = element.findAll("code");
|
18 |
+
|
19 |
+
for (let codeblock of codeblocks) {
|
20 |
+
const text = codeblock.innerText.trim();
|
21 |
+
if (text[0] === ":" && text[text.length - 1] === ":") {
|
22 |
+
const emojiEl = codeblock.createSpan({
|
23 |
+
text: ALL_EMOJIS[text] ?? text,
|
24 |
+
});
|
25 |
+
codeblock.replaceWith(emojiEl);
|
26 |
+
}
|
27 |
+
}
|
28 |
+
});
|
29 |
+
}
|
30 |
+
}
|
31 |
+
```
|
32 |
+
|
33 |
+
## Post-process Markdown code blocks
|
34 |
+
|
35 |
+
Did you know that you can create [Mermaid](https://mermaid-js.github.io/) diagrams in Obsidian by creating a `mermaid` code block with a text definition like this one?:
|
36 |
+
|
37 |
+
````md
|
38 |
+
```mermaid
|
39 |
+
flowchart LR
|
40 |
+
Start --> Stop
|
41 |
+
```
|
42 |
+
````
|
43 |
+
|
44 |
+
If you change to Preview mode, the text in the code block becomes the following diagram:
|
45 |
+
|
46 |
+
```mermaid
|
47 |
+
flowchart LR
|
48 |
+
Start --> Stop
|
49 |
+
```
|
50 |
+
|
51 |
+
If you want to add your own custom code blocks like the Mermaid one, you can use [[registerMarkdownCodeBlockProcessor|registerMarkdownCodeBlockProcessor()]]. The following example renders a code block with CSV data, as a table:
|
52 |
+
|
53 |
+
```ts
|
54 |
+
import { Plugin } from "obsidian";
|
55 |
+
|
56 |
+
export default class ExamplePlugin extends Plugin {
|
57 |
+
async onload() {
|
58 |
+
this.registerMarkdownCodeBlockProcessor("csv", (source, el, ctx) => {
|
59 |
+
const rows = source.split("\n").filter((row) => row.length > 0);
|
60 |
+
|
61 |
+
const table = el.createEl("table");
|
62 |
+
const body = table.createEl("tbody");
|
63 |
+
|
64 |
+
for (let i = 0; i < rows.length; i++) {
|
65 |
+
const cols = rows[i].split(",");
|
66 |
+
|
67 |
+
const row = body.createEl("tr");
|
68 |
+
|
69 |
+
for (let j = 0; j < cols.length; j++) {
|
70 |
+
row.createEl("td", { text: cols[j] });
|
71 |
+
}
|
72 |
+
}
|
73 |
+
});
|
74 |
+
}
|
75 |
+
}
|
76 |
+
```
|
docs/obsidian-developer/Plugins/Editor/State fields.md
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
A state field is an [[Editor extensions|editor extension]] that lets you manage custom editor state. This page walks you through building a state field by implementing a calculator extension.
|
2 |
+
|
3 |
+
The calculator should be able to add and subtract a number from the current state, and to reset the state when you want to start over.
|
4 |
+
|
5 |
+
By the end of this page, you'll understand the basic concepts of building a state field.
|
6 |
+
|
7 |
+
> [!note]
|
8 |
+
> This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more detailed information on state fields, refer to [State Fields](https://codemirror.net/docs/guide/#state-fields).
|
9 |
+
|
10 |
+
## Prerequisites
|
11 |
+
|
12 |
+
- Basic understanding of [[State management]].
|
13 |
+
|
14 |
+
## Defining state effects
|
15 |
+
|
16 |
+
State effects describe the state change you'd like to make. You may think of them as methods on a class.
|
17 |
+
|
18 |
+
In the calculator example, you'd define a state effect for each of the calculator operations:
|
19 |
+
|
20 |
+
```ts
|
21 |
+
const addEffect = StateEffect.define<number>();
|
22 |
+
const subtractEffect = StateEffect.define<number>();
|
23 |
+
const resetEffect = StateEffect.define();
|
24 |
+
```
|
25 |
+
|
26 |
+
The type between the angle brackets, `<>`, defines the input type for the effect. For example, the number you want to add or subtract. The reset effect doesn't need any input, so you can leave it out.
|
27 |
+
|
28 |
+
## Defining a state field
|
29 |
+
|
30 |
+
Contrary to what one might think, state fields don't actually _store_ state. They _manage_ it. State fields take the current state, applies any state effects, and returns the new state.
|
31 |
+
|
32 |
+
The state field contains the calculator logic to apply the mathematical operations depending on the effects in a transaction. Since a transaction can contain multiple effects, for example two additions, the state field needs to apply them all one after another.
|
33 |
+
|
34 |
+
```ts
|
35 |
+
export const calculatorField = StateField.define<number>({
|
36 |
+
create(state: EditorState): number {
|
37 |
+
return 0;
|
38 |
+
},
|
39 |
+
update(oldState: number, transaction: Transaction): number {
|
40 |
+
let newState = oldState;
|
41 |
+
|
42 |
+
for (let effect of transaction.effects) {
|
43 |
+
if (effect.is(addEffect)) {
|
44 |
+
newState += effect.value;
|
45 |
+
} else if (effect.is(subtractEffect)) {
|
46 |
+
newState -= effect.value;
|
47 |
+
} else if (effect.is(resetEffect)) {
|
48 |
+
newState = 0;
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
return newState;
|
53 |
+
},
|
54 |
+
});
|
55 |
+
```
|
56 |
+
|
57 |
+
- `create` returns the value the calculator starts with.
|
58 |
+
- `update` contains the logic for applying the effects.
|
59 |
+
- `effect.is()` lets you check the type of the effect before you apply it.
|
60 |
+
|
61 |
+
## Dispatching state effects
|
62 |
+
|
63 |
+
To apply a state effect to a state field, you need to dispatch it to the editor view as part of a transaction.
|
64 |
+
|
65 |
+
```ts
|
66 |
+
view.dispatch({
|
67 |
+
effects: [addEffect.of(num)],
|
68 |
+
});
|
69 |
+
```
|
70 |
+
|
71 |
+
You can even define a set of helper functions that provide a more familiar API:
|
72 |
+
|
73 |
+
```ts
|
74 |
+
export function add(view: EditorView, num: number) {
|
75 |
+
view.dispatch({
|
76 |
+
effects: [addEffect.of(num)],
|
77 |
+
});
|
78 |
+
}
|
79 |
+
|
80 |
+
export function subtract(view: EditorView, num: number) {
|
81 |
+
view.dispatch({
|
82 |
+
effects: [subtractEffect.of(num)],
|
83 |
+
});
|
84 |
+
}
|
85 |
+
|
86 |
+
export function reset(view: EditorView) {
|
87 |
+
view.dispatch({
|
88 |
+
effects: [resetEffect.of(null)],
|
89 |
+
});
|
90 |
+
}
|
91 |
+
```
|
92 |
+
|
93 |
+
## Next steps
|
94 |
+
|
95 |
+
Provide [[Decorations]] from your state fields to change how to display the document.
|
docs/obsidian-developer/Plugins/Editor/State management.md
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page aims to give an introduction to state management for [[Editor extensions|editor extensions]].
|
2 |
+
|
3 |
+
> [!note]
|
4 |
+
> This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more detailed information on state management, refer to [State and Updates](https://codemirror.net/docs/guide/#state-and-updates).
|
5 |
+
|
6 |
+
## State changes
|
7 |
+
|
8 |
+
In most applications, you would update state by assigning a new value to a property or variable. As a consequence, the old value is lost forever.
|
9 |
+
|
10 |
+
```ts
|
11 |
+
let note = "";
|
12 |
+
note = "Heading"
|
13 |
+
note = "# Heading"
|
14 |
+
note = "## Heading" // How to undo this?
|
15 |
+
```
|
16 |
+
|
17 |
+
To support features like undoing and redoing changes to a user's workspace, applications like Obsidian instead keep a history of all changes that have been made. To undo a change, you can then go back to a point in time before the change was made.
|
18 |
+
|
19 |
+
| | State |
|
20 |
+
|---|------------|
|
21 |
+
| 0 | |
|
22 |
+
| 1 | Heading |
|
23 |
+
| 2 | # Heading |
|
24 |
+
| 3 | ## Heading |
|
25 |
+
|
26 |
+
In TypeScript, you'd then end up with something like this:
|
27 |
+
|
28 |
+
```ts
|
29 |
+
const changes: ChangeSpec[] = [];
|
30 |
+
|
31 |
+
changes.push({ from: 0, insert: "Heading" });
|
32 |
+
changes.push({ from: 0, insert: "# " });
|
33 |
+
changes.push({ from: 0, insert: "#" });
|
34 |
+
```
|
35 |
+
|
36 |
+
## Transactions
|
37 |
+
|
38 |
+
Imagine a feature where you select some text and press the double quote, `"` to surround the selection with quotes on both sides. One way to implement the feature would be to:
|
39 |
+
|
40 |
+
1. Insert `"` at the start of the selection.
|
41 |
+
2. Insert `"` at the end of the selection.
|
42 |
+
|
43 |
+
Notice that the implementation consists of _two_ state changes. If you added these to the undo history, the user would need to undo _twice_, once for each double quote. To avoid this, what if you could group these changes so that they appear as one?
|
44 |
+
|
45 |
+
For editor extensions, a group of state changes that happen together is called a _transaction_.
|
46 |
+
|
47 |
+
If you combine what you've learned so far—and if you allow transactions that contain only a single state change—then you can consider state as a _history of transactions_.
|
48 |
+
|
49 |
+
Bringing it all together to implement the surround feature from before in an editor extension, here's how you'd add, or _dispatch_, a transaction to the editor view:
|
50 |
+
|
51 |
+
```ts
|
52 |
+
view.dispatch({
|
53 |
+
changes: [
|
54 |
+
{ from: selectionStart, insert: `"` },
|
55 |
+
{ from: selectionEnd, insert: `"` }
|
56 |
+
]
|
57 |
+
});
|
58 |
+
```
|
59 |
+
|
60 |
+
## Next steps
|
61 |
+
|
62 |
+
On this page, you've learned about modeling state as a series of state changes, and how to group them into transactions.
|
63 |
+
|
64 |
+
To learn how to manage custom state in your editor, refer to [[State fields]].
|
docs/obsidian-developer/Plugins/Editor/View plugins.md
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
A view plugin is an [[Editor extensions|editor extension]] that gives you access to the editor [[Viewport]].
|
2 |
+
|
3 |
+
> [!note]
|
4 |
+
> This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more information on state management, refer to [Affecting the View](https://codemirror.net/docs/guide/#affecting-the-view).
|
5 |
+
|
6 |
+
## Prerequisites
|
7 |
+
|
8 |
+
- Basic understanding of the [[Viewport]].
|
9 |
+
|
10 |
+
## Creating a view plugin
|
11 |
+
|
12 |
+
View plugins are editor extensions that run _after_ the viewport has been recomputed. While this means that they can access the viewport, it also means that a view plugin can't make any changes that would impact the viewport. For example, by inserting blocks or line breaks into the document.
|
13 |
+
|
14 |
+
> [!tip]
|
15 |
+
> If you want to make changes that impact the vertical layout of the editor, by for example inserting blocks and line breaks, you need to use a [[State fields|state field]].
|
16 |
+
|
17 |
+
To create a view plugin, create a class that implements [PluginValue](https://codemirror.net/docs/ref/#view.PluginValue) and pass it to the [ViewPlugin.fromClass()](https://codemirror.net/docs/ref/#view.ViewPlugin^fromClass) function.
|
18 |
+
|
19 |
+
```ts
|
20 |
+
import {
|
21 |
+
ViewUpdate,
|
22 |
+
PluginValue,
|
23 |
+
EditorView,
|
24 |
+
ViewPlugin,
|
25 |
+
} from "@codemirror/view";
|
26 |
+
|
27 |
+
class ExamplePlugin implements PluginValue {
|
28 |
+
constructor(view: EditorView) {
|
29 |
+
// ...
|
30 |
+
}
|
31 |
+
|
32 |
+
update(update: ViewUpdate) {
|
33 |
+
// ...
|
34 |
+
}
|
35 |
+
|
36 |
+
destroy() {
|
37 |
+
// ...
|
38 |
+
}
|
39 |
+
}
|
40 |
+
|
41 |
+
export const examplePlugin = ViewPlugin.fromClass(ExamplePlugin);
|
42 |
+
```
|
43 |
+
|
44 |
+
The three methods of the view plugin control its lifecycle:
|
45 |
+
|
46 |
+
- `constructor()` initializes the plugin.
|
47 |
+
- `update()` updates your plugin when something has changed, for example when the user entered or selected some text.
|
48 |
+
- `destroy()` cleans up after the plugin.
|
49 |
+
|
50 |
+
While the view plugin in the example works, it doesn't do much. If you want to better understand what causes the plugin to update, you can add a `console.log(update);` line to the `update()` method to print all updates to the console.
|
51 |
+
|
52 |
+
## Next steps
|
53 |
+
|
54 |
+
Provide [[Decorations]] from your view plugin to change how to display the document.
|
docs/obsidian-developer/Plugins/Editor/Viewport.md
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The Obsidian editor supports [huge documents](https://codemirror.net/examples/million/) with millions of lines. One of the reasons why this is possible, is because the editor only renders what's visible (and a little bit more).
|
2 |
+
|
3 |
+
Imagine that you want to edit a document that is too big to fit on your monitor. The Obsidian editor creates a "window" that moves across the document, only rendering the content within the window (and ignoring what's outside). This window is known as the editor's _viewport_.
|
4 |
+
|
5 |
+
![Viewport](viewport.svg)
|
6 |
+
|
7 |
+
Whenever the user scrolls through the document, or when the document itself changes, the viewport becomes out-of-date and needs to be recomputed.
|
8 |
+
|
9 |
+
If you want to build an editor extension that depends on the viewport, refer to [[View plugins]].
|
10 |
+
|
11 |
+
> [!note]
|
12 |
+
> This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more information on state management, refer to [Viewport](https://codemirror.net/docs/guide/#viewport).
|
docs/obsidian-developer/Plugins/Events.md
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Many of the interfaces in the Obsidian lets you subscribe to events throughout the application, for example when the user makes changes to a file.
|
2 |
+
|
3 |
+
Any registered event handlers need to be detached whenever the plugin unloads. The safest way to make sure this happens is to use the [[registerEvent|registerEvent()]] method.
|
4 |
+
|
5 |
+
```ts
|
6 |
+
import { Plugin } from "obsidian";
|
7 |
+
|
8 |
+
export default class ExamplePlugin extends Plugin {
|
9 |
+
async onload() {
|
10 |
+
this.registerEvent(this.app.vault.on('create', () => {
|
11 |
+
console.log('a new file has entered the arena')
|
12 |
+
}));
|
13 |
+
}
|
14 |
+
}
|
15 |
+
```
|
16 |
+
|
17 |
+
## Timing events
|
18 |
+
|
19 |
+
If you want to repeatedly call a function with a fixed delay, use the [`window.setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) function with the [[registerInterval|registerInterval()]] method.
|
20 |
+
|
21 |
+
The following example displays the current time in the status bar, updated every second:
|
22 |
+
|
23 |
+
```ts
|
24 |
+
import { moment, Plugin } from "obsidian";
|
25 |
+
|
26 |
+
export default class ExamplePlugin extends Plugin {
|
27 |
+
statusBar: HTMLElement;
|
28 |
+
|
29 |
+
async onload() {
|
30 |
+
this.statusBar = this.addStatusBarItem();
|
31 |
+
|
32 |
+
this.updateStatusBar();
|
33 |
+
|
34 |
+
this.registerInterval(
|
35 |
+
window.setInterval(() => this.updateStatusBar(), 1000)
|
36 |
+
);
|
37 |
+
}
|
38 |
+
|
39 |
+
updateStatusBar() {
|
40 |
+
this.statusBar.setText(moment().format("H:mm:ss"));
|
41 |
+
}
|
42 |
+
}
|
43 |
+
```
|
44 |
+
|
45 |
+
> [!tip] Date and time
|
46 |
+
> [Moment](https://momentjs.com/) is a popular JavaScript library for working with dates and time. Obsidian uses Moment internally, so you don't need to install it yourself. You can import it from the Obsidian API instead:
|
47 |
+
>
|
48 |
+
> ```ts
|
49 |
+
> import { moment } from "obsidian";
|
50 |
+
> ```
|
docs/obsidian-developer/Plugins/Getting started/Anatomy of a plugin.md
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
The [[Plugin|Plugin]] class defines the lifecycle of a plugin and exposes the operations available to all plugins:
|
2 |
+
|
3 |
+
```ts
|
4 |
+
import { Plugin } from "obsidian";
|
5 |
+
|
6 |
+
export default class ExamplePlugin extends Plugin {
|
7 |
+
async onload() {
|
8 |
+
// Configure resources needed by the plugin.
|
9 |
+
}
|
10 |
+
async onunload() {
|
11 |
+
// Release any resources configured by the plugin.
|
12 |
+
}
|
13 |
+
}
|
14 |
+
```
|
15 |
+
|
16 |
+
## Plugin lifecycle
|
17 |
+
|
18 |
+
[[onload|onload()]] runs whenever the user starts using the plugin in Obsidian. This is where you'll configure most of the plugin's capabilities.
|
19 |
+
|
20 |
+
[[onunload|onunload()]] runs when the plugin is disabled. Any resources that your plugin is using must be released here to avoid affecting the performance of Obsidian after your plugin has been disabled.
|
21 |
+
|
22 |
+
To better understand when these methods are called, you can print a message to the console whenever the plugin loads and unloads. The console is a valuable tool that lets developers monitor the status of their code.
|
23 |
+
|
24 |
+
To view the console:
|
25 |
+
|
26 |
+
1. Toggle the Developer Tools by pressing Ctrl+Shift+I in Windows and Linux, or Cmd-Option-I on macOS.
|
27 |
+
2. Click on the Console tab in the Developer Tools window.
|
28 |
+
|
29 |
+
```ts
|
30 |
+
import { Plugin } from "obsidian";
|
31 |
+
|
32 |
+
export default class ExamplePlugin extends Plugin {
|
33 |
+
async onload() {
|
34 |
+
console.log('loading plugin')
|
35 |
+
}
|
36 |
+
async onunload() {
|
37 |
+
console.log('unloading plugin')
|
38 |
+
}
|
39 |
+
}
|
40 |
+
```
|
docs/obsidian-developer/Plugins/Getting started/Build a plugin.md
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Plugins let you extend Obsidian with your own features to create a custom note-taking experience.
|
2 |
+
|
3 |
+
In this tutorial, you'll compile a sample plugin from source code and load it into Obsidian.
|
4 |
+
|
5 |
+
## What you'll learn
|
6 |
+
|
7 |
+
After you've completed this tutorial, you'll be able to:
|
8 |
+
|
9 |
+
- Configure an environment for developing Obsidian plugins.
|
10 |
+
- Compile a plugin from source code.
|
11 |
+
- Reload a plugin after making changes to it.
|
12 |
+
|
13 |
+
## Prerequisites
|
14 |
+
|
15 |
+
To complete this tutorial, you'll need:
|
16 |
+
|
17 |
+
- [Git](https://git-scm.com/) installed on your local machine.
|
18 |
+
- A local development environment for [Node.js](https://Node.js.org/en/about/).
|
19 |
+
- A code editor, such as [Visual Studio Code](https://code.visualstudio.com/).
|
20 |
+
|
21 |
+
## Before you start
|
22 |
+
|
23 |
+
When developing plugins, one mistake can lead to unintended changes to your vault. To prevent data loss, you should never develop plugins in your main vault. Always use a separate vault dedicated to plugin development.
|
24 |
+
|
25 |
+
[Create an empty vault](https://help.obsidian.md/Getting+started/Create+a+vault#Create+empty+vault).
|
26 |
+
|
27 |
+
## Step 1: Download the sample plugin
|
28 |
+
|
29 |
+
In this step, you'll download a sample plugin to the `plugins` directory in your vault's [`.obsidian` directory](https://help.obsidian.md/Advanced+topics/How+Obsidian+stores+data#Per+vault+data) so that Obsidian can find it.
|
30 |
+
|
31 |
+
The sample plugin you'll use in this tutorial is available in a [GitHub repository](https://github.com/obsidianmd/obsidian-sample-plugin).
|
32 |
+
|
33 |
+
1. Open a terminal window and change the project directory to the `plugins` directory.
|
34 |
+
|
35 |
+
```bash
|
36 |
+
cd path/to/vault
|
37 |
+
mkdir .obsidian/plugins
|
38 |
+
cd .obsidian/plugins
|
39 |
+
```
|
40 |
+
|
41 |
+
2. Clone the sample plugin using Git.
|
42 |
+
|
43 |
+
```bash
|
44 |
+
git clone https://github.com/obsidianmd/obsidian-sample-plugin.git
|
45 |
+
```
|
46 |
+
|
47 |
+
> [!tip] GitHub template repository
|
48 |
+
> The repository for the sample plugin is a GitHub template repository, which means you can create your own repository from the sample plugin. To learn how, refer to [Creating a repository from a template](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template#creating-a-repository-from-a-template).
|
49 |
+
>
|
50 |
+
> Remember to use the URL of your own repository when cloning the sample plugin.
|
51 |
+
|
52 |
+
## Step 2: Build the plugin
|
53 |
+
|
54 |
+
In this step, you'll compile the sample plugin so that Obsidian can load it.
|
55 |
+
|
56 |
+
1. Navigate to the plugin directory.
|
57 |
+
|
58 |
+
```bash
|
59 |
+
cd obsidian-sample-plugin
|
60 |
+
```
|
61 |
+
|
62 |
+
2. Install dependencies.
|
63 |
+
|
64 |
+
```bash
|
65 |
+
npm install
|
66 |
+
```
|
67 |
+
|
68 |
+
3. Compile the source code. The following command keeps running in the terminal and rebuilds the plugin when you modify the source code.
|
69 |
+
|
70 |
+
```bash
|
71 |
+
npm run dev
|
72 |
+
```
|
73 |
+
|
74 |
+
Notice that the plugin directory now has a `main.js` file that contains a compiled version of the plugin.
|
75 |
+
|
76 |
+
## Step 3: Enable the plugin
|
77 |
+
|
78 |
+
To load a plugin in Obsidian, you first need to enable it.
|
79 |
+
|
80 |
+
1. In Obsidian, open **Settings**.
|
81 |
+
2. In the side menu, select **Community plugins**.
|
82 |
+
3. Select **Turn on community plugins**.
|
83 |
+
4. Under **Installed plugins**, enable the **Sample Plugin** by selecting the toggle button next to it.
|
84 |
+
|
85 |
+
You're now ready to use the plugin in Obsidian. Next, we'll make some changes to the plugin.
|
86 |
+
|
87 |
+
## Step 4: Update the plugin manifest
|
88 |
+
|
89 |
+
In this step, you'll rename the plugin by updating the plugin manifest, `manifest.json`. The manifest contains information about your plugin, such as its name and description.
|
90 |
+
|
91 |
+
1. Open `manifest.json` in your code editor.
|
92 |
+
2. Change `id` to a unique identifier, such as `"hello-world"`.
|
93 |
+
3. Change `name` to a human-friendly name, such as `"Hello world"`.
|
94 |
+
4. Restart Obsidian to load the new changes to the plugin manifest.
|
95 |
+
|
96 |
+
Go back to **Installed plugins** and notice that the name of the plugin has been updated to reflect the changes you made.
|
97 |
+
|
98 |
+
Remember to restart Obsidian whenever you make changes to `manifest.json`.
|
99 |
+
|
100 |
+
## Step 5: Update the source code
|
101 |
+
|
102 |
+
To let the user interact with your plugin, add a _ribbon icon_ that greets the user when they select it.
|
103 |
+
|
104 |
+
1. Open `main.ts` in your code editor.
|
105 |
+
2. Rename the plugin class from `MyPlugin` to `HelloWorldPlugin`.
|
106 |
+
3. Import `Notice` from the `obsidian` package.
|
107 |
+
|
108 |
+
```ts
|
109 |
+
import { Notice, Plugin } from "obsidian";
|
110 |
+
```
|
111 |
+
|
112 |
+
4. In the `onload()` method, add the following code:
|
113 |
+
|
114 |
+
```ts
|
115 |
+
this.addRibbonIcon('dice', 'Greet', () => {
|
116 |
+
new Notice('Hello, world!');
|
117 |
+
});
|
118 |
+
```
|
119 |
+
|
120 |
+
5. In the **Command palette**, select **Reload app without saving** to reload the plugin.
|
121 |
+
|
122 |
+
You can now see a dice icon in the ribbon on the left side of the Obsidian window. Select it to display a message in the upper-right corner.
|
123 |
+
|
124 |
+
Remember, you need to **reload your plugin after changing the source code**, either by disabling it then enabling it again in the community plugins panel, or using the command palette as detailed in part 5 of this step.
|
125 |
+
|
126 |
+
> [!tip] Hot reloading
|
127 |
+
> Install the [Hot-Reload](https://github.com/pjeby/hot-reload) plugin to automatically reload your plugin while developing.
|
128 |
+
|
129 |
+
## Conclusion
|
130 |
+
|
131 |
+
In this tutorial, you've built your first Obsidian plugin using the TypeScript API. You've modified the plugin and reloaded it to reflect the changes inside Obsidian.
|
docs/obsidian-developer/Plugins/Getting started/Development workflow.md
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Whenever you make a change to the plugin source code, the plugin needs to be reloaded. You can reload the plugin by quitting Obsidian and starting it again, but that gets tiring quickly.
|
2 |
+
|
3 |
+
## Reload plugin inside Obsidian
|
4 |
+
|
5 |
+
You can reload the plugin by re-enabling it in the list of installed plugins:
|
6 |
+
|
7 |
+
1. Open **Preferences**.
|
8 |
+
2. Click **Community plugins**.
|
9 |
+
3. Find your plugin under **Installed plugins**.
|
10 |
+
4. Toggle the switch off to disable the plugin.
|
11 |
+
5. Toggle the switch on to enable the plugin.
|
12 |
+
|
13 |
+
You're now running the updated version of your plugin.
|
14 |
+
|
15 |
+
## Reload plugin on file changes
|
16 |
+
|
17 |
+
The [Hot-Reload](https://github.com/pjeby/hot-reload) plugin reloads your plugin whenever the source code changes.
|
18 |
+
|
19 |
+
For more information, check out the [forum announcement](https://forum.obsidian.md/t/plugin-release-for-developers-hot-reload-the-plugin-s-youre-developing/12185).
|
docs/obsidian-developer/Plugins/Getting started/Mobile development.md
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Learn how you can develop your plugin for mobile devices.
|
2 |
+
|
3 |
+
## Emulate mobile device on desktop
|
4 |
+
|
5 |
+
You can emulate Obsidian running a mobile device directly from the Developer Tools.
|
6 |
+
|
7 |
+
1. Open the **Developer Tools**.
|
8 |
+
2. Select the **Console** tab.
|
9 |
+
3. Enter the following and then press `Enter`.
|
10 |
+
|
11 |
+
```ts
|
12 |
+
this.app.emulateMobile(true);
|
13 |
+
```
|
14 |
+
|
15 |
+
To disable mobile emulation, enter the following and press `Enter`:
|
16 |
+
|
17 |
+
```ts
|
18 |
+
this.app.emulateMobile(false);
|
19 |
+
```
|
20 |
+
|
21 |
+
|
22 |
+
> [!tip]
|
23 |
+
> To instead toggle mobile emulation back and forth, you can use the `this.app.isMobile` flag:
|
24 |
+
>
|
25 |
+
> ```ts
|
26 |
+
> this.app.emulateMobile(!this.app.isMobile);
|
27 |
+
> ```
|
28 |
+
|
29 |
+
## Inspecting the webview on the actual mobile device
|
30 |
+
|
31 |
+
### Android
|
32 |
+
|
33 |
+
You can inspect Obsidian running on an Android device if you enable USB Debugging in Developer settings of Android. Then go to a chromium based browser on your desktop/laptop and navigate to chrome://inspect/. If you did everything right, if you have your phone/tablet connected to your PC via USB and the browser open at that link you should see your device pop up and it will let you run the usual devtools from there on it.
|
34 |
+
|
35 |
+
### iOS
|
36 |
+
|
37 |
+
You can inspect Obsidian on an iOS device running 16.4 or later and a macOS based computer. Instructions on how to set it up can be found here: https://webkit.org/web-inspector/enabling-web-inspector/
|
38 |
+
|
39 |
+
## Platform-specific features
|
40 |
+
|
41 |
+
To detect the platform your plugin is running on, you can use `Platform`:
|
42 |
+
|
43 |
+
```ts
|
44 |
+
import { Platform } from "obsidian";
|
45 |
+
|
46 |
+
if (Platform.isIosApp) {
|
47 |
+
// ...
|
48 |
+
}
|
49 |
+
|
50 |
+
if (Platform.isAndroidApp) {
|
51 |
+
// ...
|
52 |
+
}
|
53 |
+
```
|
54 |
+
|
55 |
+
## Disable your plugin on mobile devices
|
56 |
+
|
57 |
+
If your plugin requires the Node.js or Electron API, you can prevent users from installing the plugin on mobile devices.
|
58 |
+
|
59 |
+
To only support the desktop app, set `isDesktopOnly` to `true` in the [[Manifest]].
|
60 |
+
|
61 |
+
## Troubleshooting
|
62 |
+
|
63 |
+
This section lists common issues when developing for mobile devices.
|
64 |
+
|
65 |
+
### Node and Electron APIs
|
66 |
+
|
67 |
+
The Node.js API and the Electron API aren't available on mobile devices. Any calls to these libraries result cause your plugin to crash.
|
68 |
+
|
69 |
+
### Lookbehind in regular expressions
|
70 |
+
|
71 |
+
Lookbehind in regular expressions is only supported on iOS 16.4 and above, and some iPhone and iPad users may still use earlier versions. To implement a fallback for iOS users, either refer to [[#Platform-specific features]], or use a JavaScript library to detect specific browser versions.
|
72 |
+
|
73 |
+
Refer to [Can I Use](https://caniuse.com/js-regexp-lookbehind) for more information and exact version statistics. Look for "Safari on iOS".
|
docs/obsidian-developer/Plugins/Getting started/Use React in your plugin.md
ADDED
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
In this guide, you'll configure your plugin to use [React](https://react.dev/). It assumes that you already have a plugin with a [[Views|custom view]] that you want to convert to use React.
|
2 |
+
|
3 |
+
While you don't need to use a separate framework to build a plugin, there are a few reasons why you'd want to use React:
|
4 |
+
|
5 |
+
- You have existing experience of React and want to use a familiar technology.
|
6 |
+
- You have existing React components that you want to reuse in your plugin.
|
7 |
+
- Your plugin requires complex state management or other features that can be cumbersome to implement with regular [[HTML elements]].
|
8 |
+
|
9 |
+
## Configure your plugin
|
10 |
+
|
11 |
+
1. Add React to your plugin dependencies:
|
12 |
+
|
13 |
+
```bash
|
14 |
+
npm install react react-dom
|
15 |
+
```
|
16 |
+
|
17 |
+
2. Add type definitions for React:
|
18 |
+
|
19 |
+
```bash
|
20 |
+
npm install --save-dev @types/react @types/react-dom
|
21 |
+
```
|
22 |
+
|
23 |
+
3. In `tsconfig.json`, enable JSX support on the `compilerOptions` object:
|
24 |
+
|
25 |
+
```ts
|
26 |
+
{
|
27 |
+
"compilerOptions": {
|
28 |
+
"jsx": "preserve"
|
29 |
+
}
|
30 |
+
}
|
31 |
+
```
|
32 |
+
|
33 |
+
## Create a React component
|
34 |
+
|
35 |
+
Create a new file called `ReactView.tsx` in the plugin root directory, with the following content:
|
36 |
+
|
37 |
+
```tsx title="ReactView.tsx"
|
38 |
+
export const ReactView = () => {
|
39 |
+
return <h4>Hello, React!</h4>;
|
40 |
+
};
|
41 |
+
```
|
42 |
+
|
43 |
+
## Mount the React component
|
44 |
+
|
45 |
+
To use the React component, it needs to be mounted on a [[HTML elements]]. The following example mounts the `ReactView` component on the `this.containerEl.children[1]` element:
|
46 |
+
|
47 |
+
```tsx
|
48 |
+
import { StrictMode } from "react";
|
49 |
+
import { ItemView, WorkspaceLeaf } from "obsidian";
|
50 |
+
import { Root, createRoot } from "react-dom/client";
|
51 |
+
import { ReactView } from "./ReactView";
|
52 |
+
|
53 |
+
const VIEW_TYPE_EXAMPLE = "example-view";
|
54 |
+
|
55 |
+
class ExampleView extends ItemView {
|
56 |
+
root: Root | null = null;
|
57 |
+
|
58 |
+
constructor(leaf: WorkspaceLeaf) {
|
59 |
+
super(leaf);
|
60 |
+
}
|
61 |
+
|
62 |
+
getViewType() {
|
63 |
+
return VIEW_TYPE_EXAMPLE;
|
64 |
+
}
|
65 |
+
|
66 |
+
getDisplayText() {
|
67 |
+
return "Example view";
|
68 |
+
}
|
69 |
+
|
70 |
+
async onOpen() {
|
71 |
+
this.root = createRoot(this.containerEl.children[1]);
|
72 |
+
this.root.render(
|
73 |
+
<StrictMode>
|
74 |
+
<ReactView />,
|
75 |
+
</StrictMode>,
|
76 |
+
);
|
77 |
+
}
|
78 |
+
|
79 |
+
async onClose() {
|
80 |
+
this.root?.unmount();
|
81 |
+
}
|
82 |
+
}
|
83 |
+
```
|
84 |
+
|
85 |
+
For more information on `createRoot` and `unmount()`, refer to the documentation on [ReactDOM](https://react.dev/reference/react-dom/client/createRoot#root-render).
|
86 |
+
|
87 |
+
You can mount your React component on any `HTMLElement`, for example [[Plugins/User interface/Status bar|status bar items]]. Just make sure to clean up properly by calling `this.root.unmount()` when you're done.
|
88 |
+
|
89 |
+
## Create an App context
|
90 |
+
|
91 |
+
If you want to access the [[Reference/TypeScript API/App/App|App]] object from one of your React components, you need to pass it as a dependency. As your plugin grows, even though you're only using the `App` object in a few places, you start passing it through the whole component tree.
|
92 |
+
|
93 |
+
Another alternative is to create a React context for the app to make it globally available to all components inside your React view.
|
94 |
+
|
95 |
+
1. Use `createContext()` to create a new app context.
|
96 |
+
|
97 |
+
```tsx title="context.ts"
|
98 |
+
import { createContext } from "react";
|
99 |
+
import { App } from "obsidian";
|
100 |
+
|
101 |
+
export const AppContext = createContext<App | undefined>(undefined);
|
102 |
+
```
|
103 |
+
|
104 |
+
2. Wrap the `ReactView` with a context provider and pass the app as the value.
|
105 |
+
|
106 |
+
```tsx title="view.tsx"
|
107 |
+
this.root = createRoot(this.containerEl.children[1]);
|
108 |
+
this.root.render(
|
109 |
+
<AppContext.Provider value={this.app}>
|
110 |
+
<ReactView />
|
111 |
+
</AppContext.Provider>
|
112 |
+
);
|
113 |
+
```
|
114 |
+
|
115 |
+
3. Create a custom hook to make it easier to use the context in your components.
|
116 |
+
|
117 |
+
```tsx title="hooks.ts"
|
118 |
+
import { useContext } from "react";
|
119 |
+
import { AppContext } from "./context";
|
120 |
+
|
121 |
+
export const useApp = (): App | undefined => {
|
122 |
+
return useContext(AppContext);
|
123 |
+
};
|
124 |
+
```
|
125 |
+
|
126 |
+
4. Use the hook in any React component within `ReactView` to access the app.
|
127 |
+
|
128 |
+
```tsx title="ReactView.tsx"
|
129 |
+
import { useApp } from "./hooks";
|
130 |
+
|
131 |
+
export const ReactView = () => {
|
132 |
+
const { vault } = useApp();
|
133 |
+
|
134 |
+
return <h4>{vault.getName()}</h4>;
|
135 |
+
};
|
136 |
+
```
|
137 |
+
|
138 |
+
For more information, refer to the React documentation for [Passing Data Deeply with Context](https://react.dev/learn/passing-data-deeply-with-context) and [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks).
|
docs/obsidian-developer/Plugins/Getting started/Use Svelte in your plugin.md
ADDED
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This guide explains how to configure your plugin to use [Svelte](https://svelte.dev/), a light-weight alternative to traditional frameworks like React and Vue.
|
2 |
+
|
3 |
+
Svelte is built around a compiler that preprocesses your code and outputs vanilla JavaScript, which means it doesn't need to load any libraries at run time. This also means that it doesn't need a virtual DOM to track state changes, which allows your plugin to run with minimal additional overhead.
|
4 |
+
|
5 |
+
If you want to learn more about Svelte, and how to use it, refer to the [tutorial](https://svelte.dev/tutorial/basics) and the [documentation](https://svelte.dev/docs).
|
6 |
+
|
7 |
+
This guide assumes that you've finished [[Build a plugin]].
|
8 |
+
|
9 |
+
> [!tip] Visual Studio Code
|
10 |
+
> Svelte has an [official Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) that enables syntax highlighting and rich IntelliSense in Svelte components.
|
11 |
+
|
12 |
+
## Configure your plugin
|
13 |
+
|
14 |
+
To build a Svelte application, you need to install the dependencies and configure your plugin to compile code written using Svelte.
|
15 |
+
|
16 |
+
1. Add Svelte to your plugin dependencies:
|
17 |
+
|
18 |
+
```bash
|
19 |
+
npm install --save-dev svelte svelte-preprocess @tsconfig/svelte esbuild-svelte
|
20 |
+
```
|
21 |
+
|
22 |
+
2. Extend the `tsconfig.json` to enable additional type checking for common Svelte issues. The `types` property is important for TypeScript to recognize `.svelte` files.
|
23 |
+
|
24 |
+
```json
|
25 |
+
{
|
26 |
+
"extends": "@tsconfig/svelte/tsconfig.json",
|
27 |
+
"compilerOptions": {
|
28 |
+
"types": ["svelte", "node"],
|
29 |
+
|
30 |
+
// ...
|
31 |
+
}
|
32 |
+
}
|
33 |
+
```
|
34 |
+
|
35 |
+
3. Remove the following line from your `tsconfig.json` as it conflicts with the Svelte configuration.
|
36 |
+
|
37 |
+
```json
|
38 |
+
"inlineSourceMap": true,
|
39 |
+
```
|
40 |
+
|
41 |
+
4. In `esbuild.config.mjs`, add the following imports to the top of the file:
|
42 |
+
|
43 |
+
```js
|
44 |
+
import esbuildSvelte from "esbuild-svelte";
|
45 |
+
import sveltePreprocess from "svelte-preprocess";
|
46 |
+
```
|
47 |
+
|
48 |
+
5. Add Svelte to the list of plugins.
|
49 |
+
|
50 |
+
```js
|
51 |
+
esbuild
|
52 |
+
.build({
|
53 |
+
plugins: [
|
54 |
+
esbuildSvelte({
|
55 |
+
compilerOptions: { css: true },
|
56 |
+
preprocess: sveltePreprocess(),
|
57 |
+
}),
|
58 |
+
],
|
59 |
+
// ...
|
60 |
+
})
|
61 |
+
.catch(() => process.exit(1));
|
62 |
+
```
|
63 |
+
|
64 |
+
## Create a Svelte component
|
65 |
+
|
66 |
+
In the root directory of the plugin, create a new file called `Component.svelte`:
|
67 |
+
|
68 |
+
```tsx
|
69 |
+
<script lang="ts">
|
70 |
+
export let variable: number;
|
71 |
+
</script>
|
72 |
+
|
73 |
+
<div class="number">
|
74 |
+
<span>My number is {variable}!</span>
|
75 |
+
</div>
|
76 |
+
|
77 |
+
<style>
|
78 |
+
.number {
|
79 |
+
color: red;
|
80 |
+
}
|
81 |
+
</style>
|
82 |
+
```
|
83 |
+
|
84 |
+
## Mount the Svelte component
|
85 |
+
|
86 |
+
To use the Svelte component, it needs to be mounted on an existing [[HTML elements|HTML element]]. For example, if you are mounting on a custom [[ItemView|ItemView]] in Obsidian:
|
87 |
+
|
88 |
+
```ts
|
89 |
+
import { ItemView, WorkspaceLeaf } from "obsidian";
|
90 |
+
|
91 |
+
import Component from "./Component.svelte";
|
92 |
+
|
93 |
+
export const VIEW_TYPE_EXAMPLE = "example-view";
|
94 |
+
|
95 |
+
export class ExampleView extends ItemView {
|
96 |
+
component: Component;
|
97 |
+
|
98 |
+
constructor(leaf: WorkspaceLeaf) {
|
99 |
+
super(leaf);
|
100 |
+
}
|
101 |
+
|
102 |
+
getViewType() {
|
103 |
+
return VIEW_TYPE_EXAMPLE;
|
104 |
+
}
|
105 |
+
|
106 |
+
getDisplayText() {
|
107 |
+
return "Example view";
|
108 |
+
}
|
109 |
+
|
110 |
+
async onOpen() {
|
111 |
+
this.component = new Component({
|
112 |
+
target: this.contentEl,
|
113 |
+
props: {
|
114 |
+
variable: 1
|
115 |
+
}
|
116 |
+
});
|
117 |
+
}
|
118 |
+
|
119 |
+
async onClose() {
|
120 |
+
this.component.$destroy();
|
121 |
+
}
|
122 |
+
}
|
123 |
+
```
|
124 |
+
|
125 |
+
> [!info]
|
126 |
+
> Svelte requires at least TypeScript 4.5. If you see the following error when you build the plugin, you need to upgrade TypeScript to a more recent version.
|
127 |
+
>
|
128 |
+
> ```plain
|
129 |
+
> error TS5023: Unknown compiler option 'preserveValueImports'.
|
130 |
+
> ```
|
131 |
+
>
|
132 |
+
> To fix the error, run the following in your terminal:
|
133 |
+
>
|
134 |
+
> ```bash
|
135 |
+
> npm update typescript@~4.5.0
|
136 |
+
> ```
|
137 |
+
|
138 |
+
## Create a Svelte store
|
139 |
+
|
140 |
+
To create a store for your plugin and access it from within a generic Svelte component instead of passing the plugin as a prop, follow these steps:
|
141 |
+
|
142 |
+
1. Create a file called `store.ts`:
|
143 |
+
|
144 |
+
```jsx
|
145 |
+
import { writable } from "svelte/store";
|
146 |
+
import type ExamplePlugin from "./main";
|
147 |
+
|
148 |
+
const plugin = writable<ExamplePlugin>();
|
149 |
+
export default { plugin };
|
150 |
+
```
|
151 |
+
|
152 |
+
2. Configure the store:
|
153 |
+
|
154 |
+
```ts
|
155 |
+
import { ItemView, WorkspaceLeaf } from "obsidian";
|
156 |
+
import type ExamplePlugin from "./main";
|
157 |
+
import store from "./store";
|
158 |
+
import Component from "./Component.svelte";
|
159 |
+
|
160 |
+
const VIEW_TYPE_EXAMPLE = "example-view";
|
161 |
+
|
162 |
+
class ExampleView extends ItemView {
|
163 |
+
// ...
|
164 |
+
|
165 |
+
async onOpen() {
|
166 |
+
store.plugin.set(this.plugin);
|
167 |
+
|
168 |
+
this.component = new Component({
|
169 |
+
target: this.contentEl,
|
170 |
+
props: {
|
171 |
+
variable: 1
|
172 |
+
}
|
173 |
+
});
|
174 |
+
}
|
175 |
+
}
|
176 |
+
```
|
177 |
+
|
178 |
+
3. To use the store in your component:
|
179 |
+
|
180 |
+
```jsx
|
181 |
+
<script lang="ts">
|
182 |
+
import type MyPlugin from "./main";
|
183 |
+
|
184 |
+
let plugin: MyPlugin;
|
185 |
+
store.plugin.subscribe((p) => (plugin = p));
|
186 |
+
</script>
|
187 |
+
```
|
docs/obsidian-developer/Plugins/Releasing/Beta-testing plugins.md
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
Before you [[Submit your plugin|submit your plugin]], you may want to let users try it out first. While Obsidian doesn't officially support beta releases, we recommend that you use the [BRAT](https://github.com/TfTHacker/obsidian42-brat) plugin to distribute your plugin to beta testers before it's been published.
|
2 |
+
|
3 |
+
For more information, refer to the [BRAT](https://github.com/TfTHacker/obsidian42-brat/blob/main/README.md) documentation.
|
docs/obsidian-developer/Plugins/Releasing/Plugin guidelines.md
ADDED
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page lists common review comments plugin authors get when submitting their plugin.
|
2 |
+
|
3 |
+
While the guidelines on this page are recommendations, depending on their severity, we may still require you to address any violations.
|
4 |
+
|
5 |
+
> [!important] Policies for plugin developers
|
6 |
+
> Make sure that you've read our [[Developer policies]] as well as the [[Submission requirements for plugins]].
|
7 |
+
|
8 |
+
## General
|
9 |
+
|
10 |
+
### Avoid using global app instance
|
11 |
+
|
12 |
+
Avoid using the global app object, `app` (or `window.app`). Instead, use the reference provided by your plugin instance, `this.app`.
|
13 |
+
|
14 |
+
The global app object is intended for debugging purposes and might be removed in the future.
|
15 |
+
|
16 |
+
## UI text
|
17 |
+
|
18 |
+
This section lists guidelines for formatting text in the user interface, such as settings, commands, and buttons.
|
19 |
+
|
20 |
+
The example below from **Settings → Appearance** demonstrates the guidelines for text in the user interface.
|
21 |
+
|
22 |
+
![[settings-headings.png]]
|
23 |
+
|
24 |
+
1. [[#Only use headings under settings if you have more than one section.|General settings are at the top and don't have a heading]].
|
25 |
+
2. [[#Avoid "settings" in settings headings|Section headings don't have "settings" in the heading text]].
|
26 |
+
3. [[#Use Sentence case in UI]].
|
27 |
+
|
28 |
+
For more information on writing and formatting text for Obsidian, refer to our [Style guide](https://help.obsidian.md/Contributing+to+Obsidian/Style+guide).
|
29 |
+
|
30 |
+
### Only use headings under settings if you have more than one section.
|
31 |
+
|
32 |
+
Avoid adding a top-level heading in the settings tab, such as "General", "Settings", or the name of your plugin.
|
33 |
+
|
34 |
+
If you have more than one section under settings, and one contains general settings, keep them at the top without adding a heading.
|
35 |
+
|
36 |
+
For example, look at the settings under **Settings → Appearance**:
|
37 |
+
|
38 |
+
### Avoid "settings" in settings headings
|
39 |
+
|
40 |
+
In the settings tab, you can add headings to organize settings. Avoid including the word "settings" to these headings. Since everything in under the settings tab is settings, repeating it for every heading becomes redundant.
|
41 |
+
|
42 |
+
- Prefer "Advanced" over "Advanced settings".
|
43 |
+
- Prefer "Templates" over "Settings for templates".
|
44 |
+
|
45 |
+
### Use sentence case in UI
|
46 |
+
|
47 |
+
Any text in UI elements should be using [Sentence case](https://en.wiktionary.org/wiki/sentence_case) instead of [Title Case](https://en.wikipedia.org/wiki/Title_case), where only the first word in a sentence, and proper nouns, should be capitalized.
|
48 |
+
|
49 |
+
- Prefer "Template folder location" over "Template Folder Location".
|
50 |
+
- Prefer "Create new note" over "Create New Note".
|
51 |
+
|
52 |
+
## Security
|
53 |
+
|
54 |
+
### Avoid `innerHTML`, `outerHTML` and `insertAdjacentHTML`
|
55 |
+
|
56 |
+
Building DOM elements from user-defined input, using `innerHTML`, `outerHTML` and `insertAdjacentHTML` can pose a security risk.
|
57 |
+
|
58 |
+
The following example builds a DOM element using a string that contains user input, `${name}`. `name` can contain other DOM elements, such as `<script>alert()</script>`, and can allow a potential attacker to execute arbitrary code on the user's computer.
|
59 |
+
|
60 |
+
```ts
|
61 |
+
function showName(name: string) {
|
62 |
+
let containerElement = document.querySelector('.my-container');
|
63 |
+
// DON'T DO THIS
|
64 |
+
containerElement.innerHTML = `<div class="my-class"><b>Your name is: </b>${name}</div>`;
|
65 |
+
}
|
66 |
+
```
|
67 |
+
|
68 |
+
Instead, use the DOM API or the Obsidian helper functions, such as `createEl()`, `createDiv()` and `createSpan()` to build the DOM element programmatically. For more information, refer to [[HTML elements]].
|
69 |
+
|
70 |
+
## Resource management
|
71 |
+
|
72 |
+
### Clean up resources when plugin unloads
|
73 |
+
|
74 |
+
Any resources created by the plugin, such as event listeners, must be destroyed or released when the plugin unloads.
|
75 |
+
|
76 |
+
When possible, use methods like [[registerEvent|registerEvent()]] or [[addCommand|addCommand()]] to automatically clean up resources when the plugin unloads.
|
77 |
+
|
78 |
+
```ts
|
79 |
+
export default class MyPlugin extends Plugin {
|
80 |
+
onload() {
|
81 |
+
this.registerEvent(this.app.vault.on("create", this.onCreate));
|
82 |
+
}
|
83 |
+
|
84 |
+
onCreate: (file: TAbstractFile) => {
|
85 |
+
// ...
|
86 |
+
}
|
87 |
+
}
|
88 |
+
```
|
89 |
+
|
90 |
+
> [!note]
|
91 |
+
> You don't need to clean up resources that are guaranteed to be removed when your plugin unloads. For example, if you register a `mouseenter` listener on a DOM element, the event listener will be garbage-collected when the element goes out of scope.
|
92 |
+
|
93 |
+
### Don't detach leaves in `onunload`
|
94 |
+
|
95 |
+
When the user updates your plugin, any open leaves will be reinitialized at their original position, regardless of where the user had moved them.
|
96 |
+
|
97 |
+
## Commands
|
98 |
+
|
99 |
+
### Avoid setting a default hotkey for commands
|
100 |
+
|
101 |
+
Setting a default hotkey may lead to conflicts between plugins and may override hotkeys that the user has already configured.
|
102 |
+
|
103 |
+
It's also difficult to choose a default hotkey that is available on all operating systems.
|
104 |
+
|
105 |
+
### Use the appropriate callback type for commands
|
106 |
+
|
107 |
+
When you add a command in your plugin, use the appropriate callback type.
|
108 |
+
|
109 |
+
- Use `callback` if the command runs unconditionally.
|
110 |
+
- Use `checkCallback` if the command only runs under certain conditions.
|
111 |
+
|
112 |
+
If the command requires an open and active Markdown editor, use `editorCallback`, or the corresponding `editorCheckCallback`.
|
113 |
+
|
114 |
+
## Workspace
|
115 |
+
|
116 |
+
### Avoid accessing `workspace.activeLeaf` directly
|
117 |
+
|
118 |
+
If you want to access the active view, use [[getActiveViewOfType|getActiveViewOfType()]] instead:
|
119 |
+
|
120 |
+
```ts
|
121 |
+
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
122 |
+
|
123 |
+
// getActiveViewOfType will return null if the active view is null, or if it's not a MarkdownView.
|
124 |
+
if (view) {
|
125 |
+
// ...
|
126 |
+
}
|
127 |
+
```
|
128 |
+
|
129 |
+
If you want to access the editor in the active note, use `activeEditor` instead:
|
130 |
+
|
131 |
+
```ts
|
132 |
+
const editor = this.app.workspace.activeEditor;
|
133 |
+
```
|
134 |
+
|
135 |
+
### Avoid managing references to custom views
|
136 |
+
|
137 |
+
Managing references to custom view can cause memory leaks or unintended consequences.
|
138 |
+
|
139 |
+
**Don't** do this:
|
140 |
+
|
141 |
+
```ts
|
142 |
+
this.registerViewType(MY_VIEW_TYPE, () => this.view = new MyCustomView());
|
143 |
+
```
|
144 |
+
|
145 |
+
Do this instead:
|
146 |
+
|
147 |
+
```ts
|
148 |
+
this.registerViewType(MY_VIEW_TYPE, () => new MyCustomView());
|
149 |
+
```
|
150 |
+
|
151 |
+
To access the view from your plugin, use `Workspace.getActiveLeavesOfType()`:
|
152 |
+
|
153 |
+
```ts
|
154 |
+
for (let leaf of app.workspace.getActiveLeavesOfType(MY_VIEW_TYPE)) {
|
155 |
+
let view = leaf.view;
|
156 |
+
if (view instanceof MyCustomView) {
|
157 |
+
// ...
|
158 |
+
}
|
159 |
+
}
|
160 |
+
```
|
161 |
+
|
162 |
+
## Vault
|
163 |
+
|
164 |
+
### Prefer the Editor API instead of `Vault.modify`
|
165 |
+
|
166 |
+
If you want to edit an active note, use the [[Editor]] interface instead of [[Vault/modify|Vault.modify()]].
|
167 |
+
|
168 |
+
Editor maintains information about the active note, such as cursor position, selection, and folded content. When you use [[Vault/modify|Vault.modify()]] to edit the note, all that information is lost, which leads to a poor experience for the user.
|
169 |
+
|
170 |
+
Editor is also more efficient when making small changes to parts of the note.
|
171 |
+
|
172 |
+
Only use [[Vault/modify|Vault.modify()]] if you're editing a file in the background.
|
173 |
+
|
174 |
+
### Prefer the Vault API over the Adapter API
|
175 |
+
|
176 |
+
Obsidian exposes two APIs for file operations: the Vault API (`app.vault`) and the Adapter API (`app.vault.adapter`).
|
177 |
+
|
178 |
+
While the file operations in the Adapter API are often more familiar to many developers, the Vault API has two main advantages over the adapter.
|
179 |
+
|
180 |
+
- **Performance:** The Vault API has a caching layer that can speed up file reads when the file is already known to Obsidian.
|
181 |
+
- **Safety:** The Vault API performs file operations serially to avoid any race conditions, for example when reading a file that is being written to at the same time.
|
182 |
+
|
183 |
+
### Avoid iterating all files to find a file by its path
|
184 |
+
|
185 |
+
This is inefficient, especially for large vaults. Use [[Vault/getAbstractFileByPath|getAbstractFileByPath()]] instead.
|
186 |
+
|
187 |
+
**Don't** do this:
|
188 |
+
|
189 |
+
```ts
|
190 |
+
vault.getAllFiles().find(file => file.path === filePath)
|
191 |
+
```
|
192 |
+
|
193 |
+
Do this instead:
|
194 |
+
|
195 |
+
```ts
|
196 |
+
const filePath = 'folder/file.md';
|
197 |
+
|
198 |
+
const file = app.vault.getAbstractFileByPath(filePath);
|
199 |
+
|
200 |
+
// Check if it exists and is of the correct type
|
201 |
+
if (file instanceof TFile) {
|
202 |
+
// file is automatically casted to TFile within this scope.
|
203 |
+
}
|
204 |
+
```
|
205 |
+
|
206 |
+
### Use `normalizePath()` to clean up user-defined paths
|
207 |
+
|
208 |
+
Use [[normalizePath|normalizePath()]] whenever you accept user-defined paths to files or folders in the vault, or when you construct your own paths in the plugin code.
|
209 |
+
|
210 |
+
`normalizePath()` takes a path and scrubs it to be safe for the file system and for cross-platform use. This function:
|
211 |
+
|
212 |
+
- Cleans up the use of forward and backward slashes, such as replacing 1 or more of `\` or `/` with a single `/`.
|
213 |
+
- Removes leading and trailing forward and backward slashes.
|
214 |
+
- Replaces any non-breaking spaces, `\u00A0`, with a regular space.
|
215 |
+
- Runs the path through [String.prototype.normalize](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize).
|
216 |
+
|
217 |
+
```ts
|
218 |
+
import { normalizePath } from "obsidian";
|
219 |
+
const pathToPlugin = normalizePath(app.vault.configDir + "//plugins/my-plugin");
|
220 |
+
// pathToPlugin contains ".obsidian/plugins/my-plugin" not .obsidian//plugins/my-plugin
|
221 |
+
```
|
222 |
+
|
223 |
+
## Editor
|
224 |
+
|
225 |
+
### Change or reconfigure editor extensions
|
226 |
+
|
227 |
+
If you want to change or reconfigure an [[Editor extensions|editor extension]] after you've registered using [[registerEditorExtension|registerEditorExtension()]], use [[updateOptions|updateOptions()]] to update all editors.
|
228 |
+
|
229 |
+
```ts
|
230 |
+
class MyPlugin extends Plugin {
|
231 |
+
private editorExtension: Extension[] = [];
|
232 |
+
|
233 |
+
onload() {
|
234 |
+
//...
|
235 |
+
|
236 |
+
this.registerEditorExtension(this.editorExtension);
|
237 |
+
}
|
238 |
+
|
239 |
+
updateEditorExtension() {
|
240 |
+
// Empty the array while keeping the same reference
|
241 |
+
// (Don't create a new array here)
|
242 |
+
this.editorExtension.length = 0;
|
243 |
+
|
244 |
+
// Create new editor extension
|
245 |
+
let myNewExtension = this.createEditorExtension();
|
246 |
+
// Add it to the array
|
247 |
+
this.editorExtension.push(myNewExtension);
|
248 |
+
|
249 |
+
// Flush the changes to all editors
|
250 |
+
this.app.workspace.updateOptions();
|
251 |
+
}
|
252 |
+
}
|
253 |
+
|
254 |
+
```
|
255 |
+
|
256 |
+
## TypeScript
|
257 |
+
|
258 |
+
### Prefer `const` and `let` over `var`
|
259 |
+
|
260 |
+
For more information, refer to [4 Reasons Why var is Considered Obsolete in Modern JavaScript](https://javascript.plainenglish.io/4-reasons-why-var-is-considered-obsolete-in-modern-javascript-a30296b5f08f).
|
261 |
+
|
262 |
+
### Prefer async/await over Promise
|
263 |
+
|
264 |
+
Recent versions of JavaScript and TypeScript support the `async` and `await` keywords to run code asynchronously, which allow for more readable code than using Promises.
|
265 |
+
|
266 |
+
**Don't** do this:
|
267 |
+
|
268 |
+
```ts
|
269 |
+
function test(): Promise<string | null> {
|
270 |
+
return requestUrl('https://example.com')
|
271 |
+
.then(res => res.text
|
272 |
+
.catch(e => {
|
273 |
+
console.log(e);
|
274 |
+
return null;
|
275 |
+
});
|
276 |
+
}
|
277 |
+
```
|
278 |
+
|
279 |
+
Do this instead:
|
280 |
+
|
281 |
+
```ts
|
282 |
+
async function AsyncTest(): Promise<string | null> {
|
283 |
+
try {
|
284 |
+
let res = await requestUrl('https://example.com');
|
285 |
+
let text = await r.text;
|
286 |
+
return text;
|
287 |
+
}
|
288 |
+
catch (e) {
|
289 |
+
console.log(e);
|
290 |
+
return null;
|
291 |
+
}
|
292 |
+
}
|
293 |
+
```
|
294 |
+
|
295 |
+
### Consider organizing your code base using folders
|
296 |
+
|
297 |
+
If your plugin uses more than one `.ts` file, consider organizing them into folders to make it easier to review and maintain.
|
298 |
+
|
299 |
+
### Rename placeholder class names
|
300 |
+
|
301 |
+
The sample plugin contains placeholder names for common classes, such as `MyPlugin`, `MyPluginSettings`, and `SampleSettingTab`. Rename these to reflect the name of your plugin.
|
docs/obsidian-developer/Plugins/Releasing/Release your plugin with GitHub Actions.md
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Manually releasing your plugin can be time-consuming and error-prone. In this guide, you'll configure your plugin to use [GitHub Actions](https://github.com/features/actions) to automatically create a release when you create a new tag.
|
2 |
+
|
3 |
+
1. In the root directory of your plugin, create a file called `release.yml` under `.github/workflows` with the following content:
|
4 |
+
|
5 |
+
```yml
|
6 |
+
name: Release Obsidian plugin
|
7 |
+
|
8 |
+
on:
|
9 |
+
push:
|
10 |
+
tags:
|
11 |
+
- "*"
|
12 |
+
|
13 |
+
jobs:
|
14 |
+
build:
|
15 |
+
runs-on: ubuntu-latest
|
16 |
+
|
17 |
+
steps:
|
18 |
+
- uses: actions/checkout@v3
|
19 |
+
|
20 |
+
- name: Use Node.js
|
21 |
+
uses: actions/setup-node@v3
|
22 |
+
with:
|
23 |
+
node-version: "18.x"
|
24 |
+
|
25 |
+
- name: Build plugin
|
26 |
+
run: |
|
27 |
+
npm install
|
28 |
+
npm run build
|
29 |
+
|
30 |
+
- name: Create release
|
31 |
+
env:
|
32 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
33 |
+
run: |
|
34 |
+
tag="${GITHUB_REF#refs/tags/}"
|
35 |
+
|
36 |
+
gh release create "$tag" \
|
37 |
+
--title="$tag" \
|
38 |
+
--draft \
|
39 |
+
main.js manifest.json styles.css
|
40 |
+
```
|
41 |
+
|
42 |
+
2. In your terminal, commit the workflow.
|
43 |
+
|
44 |
+
```bash
|
45 |
+
git add .github/workflows/release.yml
|
46 |
+
git commit -m "Add release workflow"
|
47 |
+
git push origin main
|
48 |
+
```
|
49 |
+
|
50 |
+
3. Create a tag that matches the version in the `manifest.json` file.
|
51 |
+
|
52 |
+
```bash
|
53 |
+
git tag -a 1.0.1 -m "1.0.1"
|
54 |
+
git push origin 1.0.1
|
55 |
+
```
|
56 |
+
|
57 |
+
- `-a` creates an [annotated tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging#_creating_tags).
|
58 |
+
- `-m` specifies the name of your release. For Obsidian plugins, this must be the same as the version.
|
59 |
+
|
60 |
+
4. Browse to your repository on GitHub and select the **Actions** tab. Your workflow might still be running, or it might have finished already.
|
61 |
+
|
62 |
+
5. When the workflow finishes, go back to the main page for your repository and select **Releases** in the sidebar on the right side. The workflow has created a draft GitHub release and uploaded the required assets as binary attachments.
|
63 |
+
|
64 |
+
6. Select **Edit** (pencil icon) on the right side of the release name.
|
65 |
+
|
66 |
+
7. Add release notes to let users know what happened in this release, and then select **Publish release**.
|
67 |
+
|
68 |
+
You've successfully set up your plugin to automatically create a GitHub release whenever you create a new tag.
|
69 |
+
|
70 |
+
- If this is the first release for this plugin, you're now ready to [[Submit your plugin]].
|
71 |
+
- If this is an update to an already published plugin, your users can now update to the latest version.
|
docs/obsidian-developer/Plugins/Releasing/Submission requirements for plugins.md
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page lists extends the [[Developer policies]] with plugin-specific requirements that all plugins must follow to be published.
|
2 |
+
|
3 |
+
## Only use `fundingUrl` to link to services for financial support
|
4 |
+
|
5 |
+
Use [[Manifest#fundingUrl|fundingUrl]] if you accept financial support for your plugin, using services like Buy Me A Coffee or GitHub Sponsors.
|
6 |
+
|
7 |
+
If you don't accept donations, remove `fundingUrl` from your manifest.
|
8 |
+
|
9 |
+
## Keep plugin descriptions short and simple
|
10 |
+
|
11 |
+
Good plugin descriptions help users understand your plugin quickly and succinctly. Good descriptions often start with an action statement such as:
|
12 |
+
|
13 |
+
- "Translate selected text into..."
|
14 |
+
- "Generate notes automatically from..."
|
15 |
+
- "Import notes from..."
|
16 |
+
- "Sync highlights and annotations from..."
|
17 |
+
- "Open links in..."
|
18 |
+
|
19 |
+
Avoid starting your description with "This is a plugin", because it'll be obvious to users in the context of the Community Plugins directory.
|
20 |
+
|
21 |
+
Your description should:
|
22 |
+
|
23 |
+
- Follow the [Obsidian style guide](https://help.obsidian.md/Contributing+to+Obsidian/Style+guide).
|
24 |
+
- Have 250 characters maximum.
|
25 |
+
- End with a period `.`.
|
26 |
+
- Avoid using emoji or special characters.
|
27 |
+
- Use correct capitalization for acronyms, proper nouns and trademarks such as "Obsidian", "Markdown", "PDF". If you are not sure how to capitalize a term, refer to its website or Wikipedia description.
|
28 |
+
|
29 |
+
## Node.js and Electron APIs are only allowed on desktop
|
30 |
+
|
31 |
+
The Node.js and Electron APIs are only available in the desktop version of Obsidian. For example, Node.js packages like `fs`, `crypto`, and `os`, are only available on desktop.
|
32 |
+
|
33 |
+
If your plugin uses any of these APIs, you **must** set `isDesktopOnly` to `true` in the `manifest.json`.
|
34 |
+
|
35 |
+
> [!tip]
|
36 |
+
> Many Node.js features have Web API alternatives:
|
37 |
+
>
|
38 |
+
> - [`SubtleCrypto`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) instead of [`crypto`](https://nodejs.org/api/crypto.html).
|
39 |
+
> - `navigator.clipboard.readText()` and `navigator.clipboard.writeText()` to access clipboard contents.
|
40 |
+
|
41 |
+
## Don't include the plugin ID in the command ID
|
42 |
+
|
43 |
+
Obsidian automatically prefixes command IDs with your plugin ID. You don't need to include the plugin ID yourself.
|
docs/obsidian-developer/Plugins/Releasing/Submit your plugin.md
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
If you want to share your plugin with the Obsidian community, the best way is to submit it to the [official list of plugins](https://github.com/obsidianmd/obsidian-releases/blob/master/community-plugins.json). Once we've reviewed and published your plugin, users can install it directly from within Obsidian. It'll also be featured in the [plugin directory](https://obsidian.md/plugins) on the Obsidian website.
|
2 |
+
|
3 |
+
You only need to submit the initial version of your plugin. After your plugin has been published, users can download new releases from GitHub directly from within Obsidian.
|
4 |
+
|
5 |
+
## Prerequisites
|
6 |
+
|
7 |
+
To complete this guide, you'll need:
|
8 |
+
|
9 |
+
- A [GitHub](https://github.com/signup) account.
|
10 |
+
|
11 |
+
## Before you begin
|
12 |
+
|
13 |
+
Before you submit your plugin, make sure you have the following files in the root folder of your repository:
|
14 |
+
|
15 |
+
- A `README.md` that describes the purpose of the plugin, and how to use it.
|
16 |
+
- A `LICENSE` that determines how others are allowed to use the plugin and its source code. If you need help to [add a license](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-license-to-a-repository) for your plugin, refer to [Choose a License](https://choosealicense.com/).
|
17 |
+
- A `manifest.json` that describes your plugin. For more information, refer to [[Manifest]].
|
18 |
+
|
19 |
+
## Step 1: Publish your plugin to GitHub
|
20 |
+
|
21 |
+
> [!note] Template repositories
|
22 |
+
> If you created your plugin from one of our template repositories, you may skip this step.
|
23 |
+
|
24 |
+
To review your plugin, we need to access to the source code on GitHub. If you're unfamiliar with GitHub, refer to the GitHub docs for how to [Create a new repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository).
|
25 |
+
|
26 |
+
## Step 2: Create a release
|
27 |
+
|
28 |
+
In this step, you'll prepare a release for your plugin that's ready to be submitted.
|
29 |
+
|
30 |
+
1. In `manifest.json`, update `version` to a new version that follows the [Semantic Versioning](https://semver.org/) specification, for example `1.0.0` for your initial release. You can only use numbers and periods (`.`).
|
31 |
+
2. [Create a GitHub release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release). The "Tag version" of the release must match the version in your `manifest.json`.
|
32 |
+
3. Enter a name for the release, and describe it in the description field. Obsidian doesn't use the release name for anything, so feel free to name it however you like.
|
33 |
+
4. Upload the following plugin assets to the release as binary attachments:
|
34 |
+
|
35 |
+
- `main.js`
|
36 |
+
- `manifest.json`
|
37 |
+
- `styles.css` (optional)
|
38 |
+
|
39 |
+
## Step 3: Submit your plugin for review
|
40 |
+
|
41 |
+
In this step, you'll submit your plugin to the Obsidian team for review.
|
42 |
+
|
43 |
+
1. In [community-plugins.json](https://github.com/obsidianmd/obsidian-releases/edit/master/community-plugins.json), add a new entry at the end of the JSON array.
|
44 |
+
|
45 |
+
```json
|
46 |
+
{
|
47 |
+
"id": "doggo-dictation",
|
48 |
+
"name": "Doggo Dictation",
|
49 |
+
"author": "John Dolittle",
|
50 |
+
"description": "Transcribes dog speech into notes.",
|
51 |
+
"repo": "drdolittle/doggo-dictation"
|
52 |
+
}
|
53 |
+
```
|
54 |
+
|
55 |
+
- `id`, `name`, `author`, and `description` determines how your plugin appears to the user, and should match the corresponding properties in your [[Manifest]].
|
56 |
+
- `id` is unique to your plugin. Search `community-plugins.json` to confirm that there's no existing plugin with the same id. The `id` can't contain `obsidian`.
|
57 |
+
- `repo` is the path to your GitHub repository. For example, if your GitHub repo is located at https://github.com/your-username/your-repo-name, the path is `your-username/your-repo-name`.
|
58 |
+
|
59 |
+
Remember to add a comma after the closing brace, `}`, of the previous entry.
|
60 |
+
|
61 |
+
2. Select **Commit changes...** in the upper-right corner.
|
62 |
+
3. Select **Propose changes**.
|
63 |
+
4. Select **Create pull request**.
|
64 |
+
5. Select **Preview**, and then select **Community Plugin**.
|
65 |
+
6. Click **Create pull request**.
|
66 |
+
7. In the name of the pull request, enter "Add [...] plugin", where [...] is the name of your plugin.
|
67 |
+
8. Fill in the details in the description for the pull request. For the checkboxes, insert an `x` between the brackets, `[x]`, to mark them as done.
|
68 |
+
9. Click **Create pull request** (for the last time 🤞).
|
69 |
+
|
70 |
+
You've now submitted your plugin to the Obsidian plugin directory. Sit back and wait for an initial validation by our friendly bot. It may take a few minutes before the results are ready.
|
71 |
+
|
72 |
+
- If you see a **Ready to review** label on your PR, your submission has passed the automatic validation.
|
73 |
+
- If you see a **Validation failed** label on your PR, you need to address all listed issues until the bot assigns a **Ready to review** label.
|
74 |
+
|
75 |
+
Once your submission is ready to review, you can sit back and wait for the Obsidian team to review it.
|
76 |
+
|
77 |
+
> [!question] How long does it take to review my plugin?
|
78 |
+
> The time it takes to review your submission depends on the current workload of the Obsidian team. The team is still small, so please be patient while you wait for your plugin to be reviewed. We're currently unable to give any estimates on when we'll be able to review your submission.
|
79 |
+
|
80 |
+
## Step 4: Address review comments
|
81 |
+
|
82 |
+
Once a reviewer has reviewed your plugin, they'll add a comment to your pull request with the result of the review. The reviewer may require that you update your plugin, or they can offer suggestions on how you can improve it.
|
83 |
+
|
84 |
+
Address any required changes and update the GitHub release with the new changes. Leave a comment on the PR to let us know you've addressed the feedback. Don't open a new PR.
|
85 |
+
|
86 |
+
We'll publish the plugin as soon we've verified that all required changes have been addressed.
|
87 |
+
|
88 |
+
> [!note]
|
89 |
+
> While only Obsidian team members can publish your plugin, other community members may also offer to review your submission in the meantime.
|
90 |
+
|
91 |
+
## Next steps
|
92 |
+
|
93 |
+
Once we've reviewed and published your plugin, it's time to announce it to the community:
|
94 |
+
|
95 |
+
- Announce in [Share & showcase](https://forum.obsidian.md/c/share-showcase/9) in the forums.
|
96 |
+
- Announce in the `#updates` channel on [Discord](https://discord.gg/veuWUTm). You need the [`developer` role](https://discord.com/channels/686053708261228577/702717892533157999/830492034807758859) to post in `#updates`.
|
docs/obsidian-developer/Plugins/User interface/About user interface.md
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
This page gives you an overview of how to add or change the Obsidian user interface.
|
2 |
+
|
3 |
+
You can see some of the user interface components when you first open Obsidian.
|
4 |
+
|
5 |
+
- [[Ribbon actions]]
|
6 |
+
- [[Views]]
|
7 |
+
- [[Plugins/User interface/Status bar|Status bar]]
|
8 |
+
|
9 |
+
To modify the editor, refer to [[Editor]] and [[Editor extensions]].
|
10 |
+
|
11 |
+
![User interface](user-interface.png)
|
docs/obsidian-developer/Plugins/User interface/Commands.md
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Commands are actions that the user can perform from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) or by using a hot key.
|
2 |
+
|
3 |
+
![[command.png]]
|
4 |
+
|
5 |
+
To register a new command for your plugin, call the [[addCommand|addCommand()]] method inside the `onload()` method:
|
6 |
+
|
7 |
+
```ts
|
8 |
+
import { Plugin } from "obsidian";
|
9 |
+
|
10 |
+
export default class ExamplePlugin extends Plugin {
|
11 |
+
async onload() {
|
12 |
+
this.addCommand({
|
13 |
+
id: "print-greeting-to-console",
|
14 |
+
name: "Print greeting to console",
|
15 |
+
callback: () => {
|
16 |
+
console.log("Hey, you!");
|
17 |
+
},
|
18 |
+
});
|
19 |
+
}
|
20 |
+
}
|
21 |
+
```
|
22 |
+
|
23 |
+
## Conditional commands
|
24 |
+
|
25 |
+
If your command is only able to run under certain conditions, then consider using [[checkCallback|checkCallback()]] instead.
|
26 |
+
|
27 |
+
The `checkCallback` runs twice. First, to perform a preliminary check to determine whether the command can run. Second, to perform the action.
|
28 |
+
|
29 |
+
Since time may pass between the two runs, you need to perform the check during both calls.
|
30 |
+
|
31 |
+
To determine whether the callback should perform a preliminary check or an action, a `checking` argument is passed to the callback.
|
32 |
+
|
33 |
+
- If `checking` is set to `true`, perform a preliminary check.
|
34 |
+
- If `checking` is set to `false`, perform an action.
|
35 |
+
|
36 |
+
The command in the following example depends on a required value. In both runs, the callback checks that the value is present but only performs the action if `checking` is `false`.
|
37 |
+
|
38 |
+
```ts
|
39 |
+
this.addCommand({
|
40 |
+
id: 'example-command',
|
41 |
+
name: 'Example command',
|
42 |
+
// highlight-next-line
|
43 |
+
checkCallback: (checking: boolean) => {
|
44 |
+
const value = getRequiredValue();
|
45 |
+
|
46 |
+
if (value) {
|
47 |
+
if (!checking) {
|
48 |
+
doCommand(value);
|
49 |
+
}
|
50 |
+
|
51 |
+
return true
|
52 |
+
}
|
53 |
+
|
54 |
+
return false;
|
55 |
+
},
|
56 |
+
});
|
57 |
+
```
|
58 |
+
|
59 |
+
## Editor commands
|
60 |
+
|
61 |
+
If your command needs access to the editor, you can also use the [[editorCallback|editorCallback()]], which provides the active editor and its view as arguments.
|
62 |
+
|
63 |
+
```ts
|
64 |
+
this.addCommand({
|
65 |
+
id: 'example-command',
|
66 |
+
name: 'Example command',
|
67 |
+
editorCallback: (editor: Editor, view: MarkdownView) => {
|
68 |
+
const sel = editor.getSelection()
|
69 |
+
|
70 |
+
console.log(`You have selected: ${sel}`);
|
71 |
+
},
|
72 |
+
}
|
73 |
+
```
|
74 |
+
|
75 |
+
> [!note]
|
76 |
+
> Editor commands only appear in the Command Palette when there's an active editor available.
|
77 |
+
|
78 |
+
If the editor callback can only run given under certain conditions, consider using the [[editorCheckCallback|editorCheckCallback()]] instead. For more information, refer to [[#Conditional commands]].
|
79 |
+
|
80 |
+
```ts
|
81 |
+
this.addCommand({
|
82 |
+
id: 'example-command',
|
83 |
+
name: 'Example command',
|
84 |
+
editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => {
|
85 |
+
const value = getRequiredValue();
|
86 |
+
|
87 |
+
if (value) {
|
88 |
+
if (!checking) {
|
89 |
+
doCommand(value);
|
90 |
+
}
|
91 |
+
|
92 |
+
return true
|
93 |
+
}
|
94 |
+
|
95 |
+
return false;
|
96 |
+
},
|
97 |
+
});
|
98 |
+
```
|
99 |
+
|
100 |
+
## Hot keys
|
101 |
+
|
102 |
+
The user can run commands using a keyboard shortcut, or _hot key_. While they can configure this themselves, you can also provide a default hot key.
|
103 |
+
|
104 |
+
> [!warning]
|
105 |
+
> Avoid setting default hot keys for plugins that you intend for others to use. Hot keys are highly likely to conflict with those defined by other plugins or by the user themselves.
|
106 |
+
|
107 |
+
In this example, the user can run the command by pressing and holding Ctrl (or Cmd on Mac) and Shift together, and then pressing the letter `a` on their keyboard.
|
108 |
+
|
109 |
+
```ts
|
110 |
+
this.addCommand({
|
111 |
+
id: 'example-command',
|
112 |
+
name: 'Example command',
|
113 |
+
hotkeys: [{ modifiers: ["Mod", "Shift"], key: "a" }],
|
114 |
+
callback: () => {
|
115 |
+
console.log('Hey, you!');
|
116 |
+
},
|
117 |
+
});
|
118 |
+
```
|
119 |
+
|
120 |
+
> [!note]
|
121 |
+
> The Mod key is a special modifier key that becomes Ctrl on Windows and Linux, and Cmd on macOS.
|
docs/obsidian-developer/Plugins/User interface/Context menus.md
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
If you want to open up a context menu, use [[Menu|Menu]]:
|
2 |
+
|
3 |
+
```ts
|
4 |
+
import { Menu, Notice, Plugin } from "obsidian";
|
5 |
+
|
6 |
+
export default class ExamplePlugin extends Plugin {
|
7 |
+
async onload() {
|
8 |
+
this.addRibbonIcon("dice", "Open menu", (event) => {
|
9 |
+
const menu = new Menu();
|
10 |
+
|
11 |
+
menu.addItem((item) =>
|
12 |
+
item
|
13 |
+
.setTitle("Copy")
|
14 |
+
.setIcon("documents")
|
15 |
+
.onClick(() => {
|
16 |
+
new Notice("Copied");
|
17 |
+
})
|
18 |
+
);
|
19 |
+
|
20 |
+
menu.addItem((item) =>
|
21 |
+
item
|
22 |
+
.setTitle("Paste")
|
23 |
+
.setIcon("paste")
|
24 |
+
.onClick(() => {
|
25 |
+
new Notice("Pasted");
|
26 |
+
})
|
27 |
+
);
|
28 |
+
|
29 |
+
menu.showAtMouseEvent(event);
|
30 |
+
});
|
31 |
+
}
|
32 |
+
}
|
33 |
+
```
|
34 |
+
|
35 |
+
[[showAtMouseEvent|showAtMouseEvent()]] opens the menu where you clicked with the mouse.
|
36 |
+
|
37 |
+
> [!tip]
|
38 |
+
> If you need more control of where the menu appears, you can use `menu.showAtPosition({ x: 20, y: 20 })` to open the menu at a position relative to the top-left corner of the Obsidian window.
|
39 |
+
|
40 |
+
For more information on what icons you can use, refer to [[Plugins/User interface/Icons|Icons]].
|
41 |
+
|
42 |
+
You can also add an item to the file menu, or the editor menu, by subscribing to the `file-menu` and `editor-menu` workspace events:
|
43 |
+
|
44 |
+
![[context-menu-positions.png]]
|
45 |
+
|
46 |
+
```ts
|
47 |
+
import { Notice, Plugin } from "obsidian";
|
48 |
+
|
49 |
+
export default class ExamplePlugin extends Plugin {
|
50 |
+
async onload() {
|
51 |
+
this.registerEvent(
|
52 |
+
this.app.workspace.on("file-menu", (menu, file) => {
|
53 |
+
menu.addItem((item) => {
|
54 |
+
item
|
55 |
+
.setTitle("Print file path 👈")
|
56 |
+
.setIcon("document")
|
57 |
+
.onClick(async () => {
|
58 |
+
new Notice(file.path);
|
59 |
+
});
|
60 |
+
});
|
61 |
+
})
|
62 |
+
);
|
63 |
+
|
64 |
+
this.registerEvent(
|
65 |
+
this.app.workspace.on("editor-menu", (menu, editor, view) => {
|
66 |
+
menu.addItem((item) => {
|
67 |
+
item
|
68 |
+
.setTitle("Print file path 👈")
|
69 |
+
.setIcon("document")
|
70 |
+
.onClick(async () => {
|
71 |
+
new Notice(view.file.path);
|
72 |
+
});
|
73 |
+
});
|
74 |
+
})
|
75 |
+
);
|
76 |
+
}
|
77 |
+
}
|
78 |
+
```
|
79 |
+
|
80 |
+
For more information on handling events, refer to [[Events]].
|
docs/obsidian-developer/Plugins/User interface/HTML elements.md
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Several components in the Obsidian API, such as the [[Settings]], expose _container elements_:
|
2 |
+
|
3 |
+
```ts
|
4 |
+
import { App, PluginSettingTab } from "obsidian";
|
5 |
+
|
6 |
+
class ExampleSettingTab extends PluginSettingTab {
|
7 |
+
plugin: ExamplePlugin;
|
8 |
+
|
9 |
+
constructor(app: App, plugin: ExamplePlugin) {
|
10 |
+
super(app, plugin);
|
11 |
+
this.plugin = plugin;
|
12 |
+
}
|
13 |
+
|
14 |
+
display(): void {
|
15 |
+
// highlight-next-line
|
16 |
+
let { containerEl } = this;
|
17 |
+
|
18 |
+
// ...
|
19 |
+
}
|
20 |
+
}
|
21 |
+
```
|
22 |
+
|
23 |
+
Container elements are `HTMLElement` objects that make it possible to create custom interfaces within Obsidian.
|
24 |
+
|
25 |
+
## Create HTML elements using `createEl()`
|
26 |
+
|
27 |
+
Every `HTMLElement`, including the container element, exposes a `createEl()` method that creates an `HTMLElement` under the original element.
|
28 |
+
|
29 |
+
For example, here's how you can add an `<h1>` heading element inside the container element:
|
30 |
+
|
31 |
+
```ts
|
32 |
+
containerEl.createEl("h1", { text: "Heading 1" });
|
33 |
+
```
|
34 |
+
|
35 |
+
`createEl()` returns a reference to the new element:
|
36 |
+
|
37 |
+
```ts
|
38 |
+
const book = containerEl.createEl("div");
|
39 |
+
book.createEl("div", { text: "How to Take Smart Notes" });
|
40 |
+
book.createEl("small", { text: "Sönke Ahrens" });
|
41 |
+
```
|
42 |
+
|
43 |
+
## Style your elements
|
44 |
+
|
45 |
+
You can add custom CSS styles to your plugin by adding a `styles.css` file in the plugin root directory. To add some styles for the previous book example:
|
46 |
+
|
47 |
+
```css title="styles.css"
|
48 |
+
.book {
|
49 |
+
border: 1px solid var(--background-modifier-border);
|
50 |
+
padding: 10px;
|
51 |
+
}
|
52 |
+
|
53 |
+
.book__title {
|
54 |
+
font-weight: 600;
|
55 |
+
}
|
56 |
+
|
57 |
+
.book__author {
|
58 |
+
color: var(--text-muted);
|
59 |
+
}
|
60 |
+
```
|
61 |
+
|
62 |
+
> [!tip]
|
63 |
+
> `--background-modifier-border` and `--text-muted` are [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) that are defined and used by Obsidian itself. If you use these variables for your styles, your plugin will look great even if the user has a different theme! 🌈
|
64 |
+
|
65 |
+
To make the HTML elements use the styles, set the `cls` property for the HTML element:
|
66 |
+
|
67 |
+
```ts
|
68 |
+
const book = containerEl.createEl("div", { cls: "book" });
|
69 |
+
book.createEl("div", { text: "How to Take Smart Notes", cls: "book__title" });
|
70 |
+
book.createEl("small", { text: "Sönke Ahrens", cls: "book__author" });
|
71 |
+
```
|
72 |
+
|
73 |
+
Now it looks much better! 🎉
|
74 |
+
|
75 |
+
![[styles.png]]
|
76 |
+
|
77 |
+
### Conditional styles
|
78 |
+
|
79 |
+
Use the `toggleClass` method if you want to change the style of an element based on the user's settings or other values:
|
80 |
+
|
81 |
+
```ts
|
82 |
+
element.toggleClass("danger", status === "error");
|
83 |
+
```
|
docs/obsidian-developer/Plugins/User interface/Icons.md
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Several of the UI components in the Obsidian API lets you configure an accompanying icon. You can choose from one of the built-in icons, or you can add your own.
|
2 |
+
|
3 |
+
## Browse available icons
|
4 |
+
|
5 |
+
Browse to [lucide.dev](https://lucide.dev/) to see all available icons and their corresponding names.
|
6 |
+
|
7 |
+
**Please note:** Only icons up to v0.171.0 are supported at this time.
|
8 |
+
|
9 |
+
## Use icons
|
10 |
+
|
11 |
+
If you'd like to use icons in your custom interfaces, use the [[setIcon|setIcon()]] utility function to add an icon to an [[HTML elements|HTML element]]. The following example adds icon to the status bar:
|
12 |
+
|
13 |
+
```ts
|
14 |
+
import { Plugin, setIcon } from "obsidian";
|
15 |
+
|
16 |
+
export default class ExamplePlugin extends Plugin {
|
17 |
+
async onload() {
|
18 |
+
const item = this.addStatusBarItem();
|
19 |
+
setIcon(item, "info");
|
20 |
+
}
|
21 |
+
}
|
22 |
+
```
|
23 |
+
|
24 |
+
To change the size of the icon, set the `--icon-size` [[Reference/CSS variables/Foundations/Icons|CSS variable]] on the element containing the icon using preset sizes:
|
25 |
+
|
26 |
+
```css
|
27 |
+
div {
|
28 |
+
--icon-size: var(--icon-size-m);
|
29 |
+
}
|
30 |
+
```
|
31 |
+
|
32 |
+
## Add your own icon
|
33 |
+
|
34 |
+
To add a custom icon for your plugin, use the [[addIcon|addIcon()]] utility:
|
35 |
+
|
36 |
+
```ts
|
37 |
+
import { addIcon, Plugin } from "obsidian";
|
38 |
+
|
39 |
+
export default class ExamplePlugin extends Plugin {
|
40 |
+
async onload() {
|
41 |
+
addIcon("circle", `<circle cx="50" cy="50" r="50" fill="currentColor" />`);
|
42 |
+
|
43 |
+
this.addRibbonIcon("circle", "Click me", () => {
|
44 |
+
console.log("Hello, you!");
|
45 |
+
});
|
46 |
+
}
|
47 |
+
}
|
48 |
+
```
|
49 |
+
|
50 |
+
`addIcon` takes two arguments:
|
51 |
+
|
52 |
+
1. A name to uniquely identify your icon.
|
53 |
+
2. The SVG content for the icon, without the surrounding `<svg>` tag.
|
54 |
+
|
55 |
+
Note that your icon needs to fit within a `0 0 100 100` view box to be drawn properly.
|
56 |
+
|
57 |
+
After the call to `addIcon`, you can use the icon just like any of the built-in icons.
|
58 |
+
|
59 |
+
### Icon design guidelines
|
60 |
+
|
61 |
+
For compatibility and cohesiveness with the Obsidian interface, your icons should [follow Lucide’s guidelines](https://lucide.dev/guide/design/icon-design-guide):
|
62 |
+
|
63 |
+
- Icons must be designed on a 24 by 24 pixels canvas
|
64 |
+
- Icons must have at least 1 pixel padding within the canvas
|
65 |
+
- Icons must have a stroke width of 2 pixels
|
66 |
+
- Icons must use round joins
|
67 |
+
- Icons must use round caps
|
68 |
+
- Icons must use centered strokes
|
69 |
+
- Shapes (such as rectangles) in icons must have border radius of 2 pixels
|
70 |
+
- Distinct elements must have 2 pixels of spacing between each other
|
71 |
+
|
72 |
+
Lucide also [provides templates and guides](https://github.com/lucide-icons/lucide/blob/main/CONTRIBUTING.md) for vector editors such as Illustrator, Figma, and Inkscape.
|
docs/obsidian-developer/Plugins/User interface/Modals.md
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Modals display information and accept input from the user. To create a modal, create a class that extends [[Reference/TypeScript API/Modal/Modal|Modal]]:
|
2 |
+
|
3 |
+
```ts
|
4 |
+
import { App, Modal } from "obsidian";
|
5 |
+
|
6 |
+
export class ExampleModal extends Modal {
|
7 |
+
constructor(app: App) {
|
8 |
+
super(app);
|
9 |
+
}
|
10 |
+
|
11 |
+
onOpen() {
|
12 |
+
let { contentEl } = this;
|
13 |
+
contentEl.setText("Look at me, I'm a modal! 👀");
|
14 |
+
}
|
15 |
+
|
16 |
+
onClose() {
|
17 |
+
let { contentEl } = this;
|
18 |
+
contentEl.empty();
|
19 |
+
}
|
20 |
+
}
|
21 |
+
```
|
22 |
+
|
23 |
+
- [[Reference/TypeScript API/View/onOpen|onOpen()]] is called when the modal is opened and is responsible for building the content of your modal. For more information, refer to [HTML elements](HTML%20elements.md).
|
24 |
+
- [[Reference/TypeScript API/Modal/onClose|onClose()]] is called when the modal is closed and is responsible for cleaning up any resources used by the modal.
|
25 |
+
|
26 |
+
To open a modal, create a new instance of `ExampleModal` and call [[Reference/TypeScript API/Modal/open|open()]] on it:
|
27 |
+
|
28 |
+
```ts
|
29 |
+
import { Plugin } from "obsidian";
|
30 |
+
import { ExampleModal } from "./modal";
|
31 |
+
|
32 |
+
export default class ExamplePlugin extends Plugin {
|
33 |
+
async onload() {
|
34 |
+
this.addCommand({
|
35 |
+
id: "display-modal",
|
36 |
+
name: "Display modal",
|
37 |
+
callback: () => {
|
38 |
+
new ExampleModal(this.app).open();
|
39 |
+
},
|
40 |
+
});
|
41 |
+
}
|
42 |
+
}
|
43 |
+
```
|
44 |
+
|
45 |
+
## Accept user input
|
46 |
+
|
47 |
+
The modal in the previous example only displayed some text. Let's look at a little more complex example that handles input from the user.
|
48 |
+
|
49 |
+
![[modal-input.png]]
|
50 |
+
|
51 |
+
```ts
|
52 |
+
import { App, Modal, Setting } from "obsidian";
|
53 |
+
|
54 |
+
export class ExampleModal extends Modal {
|
55 |
+
result: string;
|
56 |
+
onSubmit: (result: string) => void;
|
57 |
+
|
58 |
+
constructor(app: App, onSubmit: (result: string) => void) {
|
59 |
+
super(app);
|
60 |
+
this.onSubmit = onSubmit;
|
61 |
+
}
|
62 |
+
|
63 |
+
onOpen() {
|
64 |
+
const { contentEl } = this;
|
65 |
+
|
66 |
+
contentEl.createEl("h1", { text: "What's your name?" });
|
67 |
+
|
68 |
+
new Setting(contentEl)
|
69 |
+
.setName("Name")
|
70 |
+
.addText((text) =>
|
71 |
+
text.onChange((value) => {
|
72 |
+
this.result = value
|
73 |
+
}));
|
74 |
+
|
75 |
+
new Setting(contentEl)
|
76 |
+
.addButton((btn) =>
|
77 |
+
btn
|
78 |
+
.setButtonText("Submit")
|
79 |
+
.setCta()
|
80 |
+
.onClick(() => {
|
81 |
+
this.close();
|
82 |
+
this.onSubmit(this.result);
|
83 |
+
}));
|
84 |
+
}
|
85 |
+
|
86 |
+
onClose() {
|
87 |
+
let { contentEl } = this;
|
88 |
+
contentEl.empty();
|
89 |
+
}
|
90 |
+
}
|
91 |
+
```
|
92 |
+
|
93 |
+
The result is stored in `this.result` and returned in the `onSubmit` callback when the user clicks **Submit**:
|
94 |
+
|
95 |
+
```ts
|
96 |
+
new ExampleModal(this.app, (result) => {
|
97 |
+
new Notice(`Hello, ${result}!`);
|
98 |
+
}).open();
|
99 |
+
```
|
100 |
+
|
101 |
+
## Select from list of suggestions
|
102 |
+
|
103 |
+
[[SuggestModal|SuggestModal]] is a special modal that lets you display a list of suggestions to the user.
|
104 |
+
|
105 |
+
![[suggest-modal.gif]]
|
106 |
+
|
107 |
+
```ts
|
108 |
+
import { App, Notice, SuggestModal } from "obsidian";
|
109 |
+
|
110 |
+
interface Book {
|
111 |
+
title: string;
|
112 |
+
author: string;
|
113 |
+
}
|
114 |
+
|
115 |
+
const ALL_BOOKS = [
|
116 |
+
{
|
117 |
+
title: "How to Take Smart Notes",
|
118 |
+
author: "Sönke Ahrens",
|
119 |
+
},
|
120 |
+
{
|
121 |
+
title: "Thinking, Fast and Slow",
|
122 |
+
author: "Daniel Kahneman",
|
123 |
+
},
|
124 |
+
{
|
125 |
+
title: "Deep Work",
|
126 |
+
author: "Cal Newport",
|
127 |
+
},
|
128 |
+
];
|
129 |
+
|
130 |
+
export class ExampleModal extends SuggestModal<Book> {
|
131 |
+
// Returns all available suggestions.
|
132 |
+
getSuggestions(query: string): Book[] {
|
133 |
+
return ALL_BOOKS.filter((book) =>
|
134 |
+
book.title.toLowerCase().includes(query.toLowerCase())
|
135 |
+
);
|
136 |
+
}
|
137 |
+
|
138 |
+
// Renders each suggestion item.
|
139 |
+
renderSuggestion(book: Book, el: HTMLElement) {
|
140 |
+
el.createEl("div", { text: book.title });
|
141 |
+
el.createEl("small", { text: book.author });
|
142 |
+
}
|
143 |
+
|
144 |
+
// Perform action on the selected suggestion.
|
145 |
+
onChooseSuggestion(book: Book, evt: MouseEvent | KeyboardEvent) {
|
146 |
+
new Notice(`Selected ${book.title}`);
|
147 |
+
}
|
148 |
+
}
|
149 |
+
```
|
150 |
+
|
151 |
+
In addition to `SuggestModal`, the Obsidian API provides an even more specialized type of modal for suggestions: the [[FuzzySuggestModal|FuzzySuggestModal]]. While it doesn't give you the same control of how each item is rendered, you get [fuzzy string search](https://en.wikipedia.org/wiki/Approximate_string_matching) out-of-the-box.
|
152 |
+
|
153 |
+
![[fuzzy-suggestion-modal.png]]
|
154 |
+
|
155 |
+
```ts
|
156 |
+
export class ExampleModal extends FuzzySuggestModal<Book> {
|
157 |
+
getItems(): Book[] {
|
158 |
+
return ALL_BOOKS;
|
159 |
+
}
|
160 |
+
|
161 |
+
getItemText(book: Book): string {
|
162 |
+
return book.title;
|
163 |
+
}
|
164 |
+
|
165 |
+
onChooseItem(book: Book, evt: MouseEvent | KeyboardEvent) {
|
166 |
+
new Notice(`Selected ${book.title}`);
|
167 |
+
}
|
168 |
+
}
|
169 |
+
```
|