obsidian-dev-skills 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/obsidian-dev-plugins/SKILL.md +35 -0
- package/obsidian-dev-plugins/references/agent-dos-donts.md +57 -0
- package/obsidian-dev-plugins/references/code-patterns.md +852 -0
- package/obsidian-dev-plugins/references/coding-conventions.md +21 -0
- package/obsidian-dev-plugins/references/commands-settings.md +24 -0
- package/obsidian-dev-plugins/references/common-tasks.md +429 -0
- package/obsidian-dev-themes/SKILL.md +34 -0
- package/obsidian-dev-themes/references/theme-best-practices.md +50 -0
- package/obsidian-dev-themes/references/theme-coding-conventions.md +45 -0
- package/package.json +11 -3
- package/scripts/init.mjs +113 -0
- package/scripts/setup-local.ps1 +52 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Source: Complete examples from obsidian-sample-plugin, obsidian-plugin-docs, and obsidian-api (API is authoritative)
|
|
3
|
+
Last synced: See sync-status.json for authoritative sync dates
|
|
4
|
+
Update frequency: Check reference repos for new patterns
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Code Patterns
|
|
8
|
+
|
|
9
|
+
Comprehensive code patterns for common Obsidian plugin development tasks. **Always verify API details in `.ref/obsidian-api/obsidian.d.ts`** - it's the authoritative source and may have features not yet documented in plugin docs.
|
|
10
|
+
|
|
11
|
+
**When to use this vs [common-tasks.md](common-tasks.md)**:
|
|
12
|
+
- **code-patterns.md**: Complete, production-ready examples with full context, error handling, and best practices
|
|
13
|
+
- **common-tasks.md**: Quick snippets and basic patterns for simple operations
|
|
14
|
+
|
|
15
|
+
## Complete Settings Tab
|
|
16
|
+
|
|
17
|
+
**Source**: Based on `.ref/obsidian-sample-plugin/main.ts`, `.ref/obsidian-plugin-docs/docs/guides/settings.md`, and `.ref/obsidian-api/obsidian.d.ts`
|
|
18
|
+
|
|
19
|
+
**Note**: `SettingGroup` is available in the API since 1.11.0 but may not be documented in plugin docs yet. Always check the API first.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { App, PluginSettingTab, Setting } from "obsidian";
|
|
23
|
+
|
|
24
|
+
interface MyPluginSettings {
|
|
25
|
+
textSetting: string;
|
|
26
|
+
toggleSetting: boolean;
|
|
27
|
+
dropdownSetting: string;
|
|
28
|
+
sliderValue: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
32
|
+
textSetting: "default",
|
|
33
|
+
toggleSetting: true,
|
|
34
|
+
dropdownSetting: "option1",
|
|
35
|
+
sliderValue: 50,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class MySettingTab extends PluginSettingTab {
|
|
39
|
+
plugin: MyPlugin;
|
|
40
|
+
|
|
41
|
+
constructor(app: App, plugin: MyPlugin) {
|
|
42
|
+
super(app, plugin);
|
|
43
|
+
this.plugin = plugin;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
display(): void {
|
|
47
|
+
const { containerEl } = this;
|
|
48
|
+
containerEl.empty();
|
|
49
|
+
|
|
50
|
+
// Text input
|
|
51
|
+
new Setting(containerEl)
|
|
52
|
+
.setName("Text setting")
|
|
53
|
+
.setDesc("Description of text setting")
|
|
54
|
+
.addText((text) =>
|
|
55
|
+
text
|
|
56
|
+
.setPlaceholder("Enter text")
|
|
57
|
+
.setValue(this.plugin.settings.textSetting)
|
|
58
|
+
.onChange(async (value) => {
|
|
59
|
+
this.plugin.settings.textSetting = value;
|
|
60
|
+
await this.plugin.saveSettings();
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Toggle
|
|
65
|
+
new Setting(containerEl)
|
|
66
|
+
.setName("Toggle setting")
|
|
67
|
+
.setDesc("Enable or disable feature")
|
|
68
|
+
.addToggle((toggle) =>
|
|
69
|
+
toggle
|
|
70
|
+
.setValue(this.plugin.settings.toggleSetting)
|
|
71
|
+
.onChange(async (value) => {
|
|
72
|
+
this.plugin.settings.toggleSetting = value;
|
|
73
|
+
await this.plugin.saveSettings();
|
|
74
|
+
this.display(); // Re-render if toggle affects other settings
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Dropdown
|
|
79
|
+
new Setting(containerEl)
|
|
80
|
+
.setName("Dropdown setting")
|
|
81
|
+
.setDesc("Select an option")
|
|
82
|
+
.addDropdown((dropdown) =>
|
|
83
|
+
dropdown
|
|
84
|
+
.addOption("option1", "Option 1")
|
|
85
|
+
.addOption("option2", "Option 2")
|
|
86
|
+
.addOption("option3", "Option 3")
|
|
87
|
+
.setValue(this.plugin.settings.dropdownSetting)
|
|
88
|
+
.onChange(async (value) => {
|
|
89
|
+
this.plugin.settings.dropdownSetting = value;
|
|
90
|
+
await this.plugin.saveSettings();
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Slider
|
|
95
|
+
new Setting(containerEl)
|
|
96
|
+
.setName("Slider setting")
|
|
97
|
+
.setDesc(`Value: ${this.plugin.settings.sliderValue}`)
|
|
98
|
+
.addSlider((slider) =>
|
|
99
|
+
slider
|
|
100
|
+
.setLimits(0, 100, 1)
|
|
101
|
+
.setValue(this.plugin.settings.sliderValue)
|
|
102
|
+
.setDynamicTooltip()
|
|
103
|
+
.onChange(async (value) => {
|
|
104
|
+
this.plugin.settings.sliderValue = value;
|
|
105
|
+
await this.plugin.saveSettings();
|
|
106
|
+
this.display(); // Update description
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Setting with extra button
|
|
111
|
+
new Setting(containerEl)
|
|
112
|
+
.setName("Setting with reset")
|
|
113
|
+
.addText((text) =>
|
|
114
|
+
text.setValue(this.plugin.settings.textSetting)
|
|
115
|
+
)
|
|
116
|
+
.addExtraButton((btn) =>
|
|
117
|
+
btn
|
|
118
|
+
.setIcon("reset")
|
|
119
|
+
.setTooltip("Reset to default")
|
|
120
|
+
.onClick(async () => {
|
|
121
|
+
this.plugin.settings.textSetting = DEFAULT_SETTINGS.textSetting;
|
|
122
|
+
await this.plugin.saveSettings();
|
|
123
|
+
this.display();
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// In main plugin class:
|
|
130
|
+
this.addSettingTab(new MySettingTab(this.app, this));
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Settings with Groups (Conditional / Backward Compatible)
|
|
134
|
+
|
|
135
|
+
**Source**: Based on `.ref/obsidian-api/obsidian.d.ts` (API is authoritative) - `SettingGroup` requires API 1.11.0+
|
|
136
|
+
|
|
137
|
+
**Use this when**: You want to use `SettingGroup` for users on Obsidian 1.11.0+ while still supporting older versions. This provides conditional settings groups that automatically use the modern API when available, with a fallback for older versions.
|
|
138
|
+
|
|
139
|
+
**Note**: Use the backward compatibility approach below to support both users on Obsidian 1.11.0+ and users on older versions. Alternatively, you can choose to:
|
|
140
|
+
- Continue using the compatibility utility (supports all versions)
|
|
141
|
+
- Force `minAppVersion: "1.11.0"` in `manifest.json` and use `SettingGroup` directly (simpler, but excludes older versions)
|
|
142
|
+
|
|
143
|
+
### Step 1: Create the Compatibility Utility
|
|
144
|
+
|
|
145
|
+
Create `src/utils/settings-compat.ts` (or wherever you keep utilities):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
/**
|
|
149
|
+
* Compatibility utilities for settings
|
|
150
|
+
* Provides backward compatibility for SettingGroup (requires API 1.11.0+)
|
|
151
|
+
*/
|
|
152
|
+
import { Setting, requireApiVersion } from 'obsidian';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Type definition for SettingGroup constructor
|
|
156
|
+
* Note: SettingGroup may exist at runtime in 1.11.0+ but may not be in TypeScript definitions
|
|
157
|
+
*
|
|
158
|
+
* IMPORTANT: This type signature is inferred from usage patterns. When .ref/obsidian-api/obsidian.d.ts
|
|
159
|
+
* is available, verify the actual signature there. The signature shown here matches the expected
|
|
160
|
+
* behavior based on Obsidian's API design patterns.
|
|
161
|
+
*/
|
|
162
|
+
type SettingGroupConstructor = new (containerEl: HTMLElement) => {
|
|
163
|
+
setHeading(heading: string): {
|
|
164
|
+
addSetting(cb: (setting: Setting) => void): void;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Interface that works with both SettingGroup and fallback container
|
|
170
|
+
*/
|
|
171
|
+
export interface SettingsContainer {
|
|
172
|
+
addSetting(cb: (setting: Setting) => void): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Creates a settings container that uses SettingGroup if available (API 1.11.0+),
|
|
177
|
+
* otherwise falls back to creating a heading and using the container directly.
|
|
178
|
+
*
|
|
179
|
+
* Uses requireApiVersion('1.11.0') to check if SettingGroup is available.
|
|
180
|
+
* This is the official Obsidian API method for version checking.
|
|
181
|
+
*
|
|
182
|
+
* IMPORTANT: We use dynamic require() instead of direct import because SettingGroup
|
|
183
|
+
* may not be in TypeScript type definitions even if it exists at runtime in 1.11.0+.
|
|
184
|
+
* This avoids compile-time TypeScript errors while still working at runtime.
|
|
185
|
+
*
|
|
186
|
+
* @param containerEl - The container element for settings
|
|
187
|
+
* @param heading - The heading text for the settings group (optional)
|
|
188
|
+
* @param manifestId - The plugin's manifest ID for CSS scoping (required for fallback mode)
|
|
189
|
+
* @returns A container that can be used to add settings
|
|
190
|
+
*/
|
|
191
|
+
export function createSettingsGroup(
|
|
192
|
+
containerEl: HTMLElement,
|
|
193
|
+
heading?: string,
|
|
194
|
+
manifestId?: string
|
|
195
|
+
): SettingsContainer {
|
|
196
|
+
// Check if SettingGroup is available (API 1.11.0+)
|
|
197
|
+
// requireApiVersion is the official Obsidian API method for version checking
|
|
198
|
+
if (requireApiVersion('1.11.0')) {
|
|
199
|
+
// Use dynamic require() to access SettingGroup at runtime
|
|
200
|
+
// This avoids TypeScript errors when SettingGroup isn't in type definitions
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
202
|
+
const obsidian = require('obsidian');
|
|
203
|
+
const SettingGroup = obsidian.SettingGroup as SettingGroupConstructor;
|
|
204
|
+
|
|
205
|
+
// Use SettingGroup - it's guaranteed to exist if requireApiVersion returns true
|
|
206
|
+
const group = heading
|
|
207
|
+
? new SettingGroup(containerEl).setHeading(heading)
|
|
208
|
+
: new SettingGroup(containerEl);
|
|
209
|
+
return {
|
|
210
|
+
addSetting(cb: (setting: Setting) => void) {
|
|
211
|
+
group.addSetting(cb);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
// Fallback path (either API < 1.11.0 or SettingGroup not found)
|
|
216
|
+
// Add scoping class to containerEl to scope CSS to only this plugin's settings
|
|
217
|
+
if (manifestId) {
|
|
218
|
+
containerEl.addClass(`${manifestId}-settings-compat`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fallback: Create a heading manually and use container directly
|
|
222
|
+
if (heading) {
|
|
223
|
+
const headingEl = containerEl.createDiv('setting-group-heading');
|
|
224
|
+
headingEl.createEl('h3', { text: heading });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
addSetting(cb: (setting: Setting) => void) {
|
|
229
|
+
const setting = new Setting(containerEl);
|
|
230
|
+
cb(setting);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Note**: The dynamic `require()` approach is necessary because `SettingGroup` may not be in TypeScript type definitions even if it exists at runtime in Obsidian 1.11.0+. This avoids compile-time TypeScript errors while maintaining runtime compatibility.
|
|
238
|
+
|
|
239
|
+
### Step 2: Use in Settings Tab
|
|
240
|
+
|
|
241
|
+
Update your settings tab to use the compatibility utility:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { App, PluginSettingTab, Setting } from "obsidian";
|
|
245
|
+
import { createSettingsGroup } from "./utils/settings-compat";
|
|
246
|
+
|
|
247
|
+
interface MyPluginSettings {
|
|
248
|
+
generalEnabled: boolean;
|
|
249
|
+
generalTimeout: number;
|
|
250
|
+
advancedDebug: boolean;
|
|
251
|
+
advancedLogLevel: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
255
|
+
generalEnabled: true,
|
|
256
|
+
generalTimeout: 5000,
|
|
257
|
+
advancedDebug: false,
|
|
258
|
+
advancedLogLevel: "info",
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
class MySettingTab extends PluginSettingTab {
|
|
262
|
+
plugin: MyPlugin;
|
|
263
|
+
|
|
264
|
+
constructor(app: App, plugin: MyPlugin) {
|
|
265
|
+
super(app, plugin);
|
|
266
|
+
this.plugin = plugin;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
display(): void {
|
|
270
|
+
const { containerEl } = this;
|
|
271
|
+
containerEl.empty();
|
|
272
|
+
|
|
273
|
+
// General Settings Group
|
|
274
|
+
const generalGroup = createSettingsGroup(containerEl, "General Settings", "my-plugin");
|
|
275
|
+
|
|
276
|
+
generalGroup.addSetting((setting) => {
|
|
277
|
+
setting
|
|
278
|
+
.setName("Enable feature")
|
|
279
|
+
.setDesc("Enable or disable the main feature")
|
|
280
|
+
.addToggle((toggle) => {
|
|
281
|
+
toggle
|
|
282
|
+
.setValue(this.plugin.settings.generalEnabled)
|
|
283
|
+
.onChange(async (value) => {
|
|
284
|
+
this.plugin.settings.generalEnabled = value;
|
|
285
|
+
await this.plugin.saveSettings();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
generalGroup.addSetting((setting) => {
|
|
291
|
+
setting
|
|
292
|
+
.setName("Timeout")
|
|
293
|
+
.setDesc("Timeout in milliseconds")
|
|
294
|
+
.addSlider((slider) => {
|
|
295
|
+
slider
|
|
296
|
+
.setLimits(1000, 10000, 500)
|
|
297
|
+
.setValue(this.plugin.settings.generalTimeout)
|
|
298
|
+
.setDynamicTooltip()
|
|
299
|
+
.onChange(async (value) => {
|
|
300
|
+
this.plugin.settings.generalTimeout = value;
|
|
301
|
+
await this.plugin.saveSettings();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Advanced Settings Group
|
|
307
|
+
const advancedGroup = createSettingsGroup(containerEl, "Advanced Settings", "my-plugin");
|
|
308
|
+
|
|
309
|
+
advancedGroup.addSetting((setting) => {
|
|
310
|
+
setting
|
|
311
|
+
.setName("Debug mode")
|
|
312
|
+
.setDesc("Enable debug logging")
|
|
313
|
+
.addToggle((toggle) => {
|
|
314
|
+
toggle
|
|
315
|
+
.setValue(this.plugin.settings.advancedDebug)
|
|
316
|
+
.onChange(async (value) => {
|
|
317
|
+
this.plugin.settings.advancedDebug = value;
|
|
318
|
+
await this.plugin.saveSettings();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
advancedGroup.addSetting((setting) => {
|
|
324
|
+
setting
|
|
325
|
+
.setName("Log level")
|
|
326
|
+
.setDesc("Set the logging level")
|
|
327
|
+
.addDropdown((dropdown) => {
|
|
328
|
+
dropdown
|
|
329
|
+
.addOption("info", "Info")
|
|
330
|
+
.addOption("warn", "Warning")
|
|
331
|
+
.addOption("error", "Error")
|
|
332
|
+
.setValue(this.plugin.settings.advancedLogLevel)
|
|
333
|
+
.onChange(async (value) => {
|
|
334
|
+
this.plugin.settings.advancedLogLevel = value;
|
|
335
|
+
await this.plugin.saveSettings();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// In main plugin class:
|
|
343
|
+
this.addSettingTab(new MySettingTab(this.app, this));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Step 3: Add CSS Styling (Required for Older Obsidian Builds)
|
|
347
|
+
|
|
348
|
+
**Important**: When using the compatibility utility for older Obsidian builds (< 1.11.0), you must add CSS to prevent double divider lines. The fallback creates a heading with class `setting-group-heading`, and without proper CSS, you'll see a double divider (one from the heading's border-bottom and one from the first setting-item's border-top).
|
|
349
|
+
|
|
350
|
+
**CRITICAL**: The CSS **MUST** be scoped to your plugin's settings container using a manifest-ID-based class to avoid affecting other plugins' settings. Global CSS selectors will impact all settings in Obsidian, not just your plugin's settings.
|
|
351
|
+
|
|
352
|
+
Add this CSS to your `styles.css` file, replacing `{manifest-id}` with your plugin's manifest ID:
|
|
353
|
+
|
|
354
|
+
```css
|
|
355
|
+
/* Group settings compatibility styling for older Obsidian builds (< 1.11.0) */
|
|
356
|
+
/* Scoped to only this plugin's settings container to avoid affecting other plugins */
|
|
357
|
+
.{manifest-id}-settings-compat .setting-group-heading h3 {
|
|
358
|
+
margin: 0 0 0.75rem;
|
|
359
|
+
padding-bottom: 0.5rem;
|
|
360
|
+
padding-top: 0.5rem;
|
|
361
|
+
font-size: 1rem;
|
|
362
|
+
font-weight: 600;
|
|
363
|
+
border-bottom: none !important;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Example**: If your manifest ID is `sample-plugin`, use `.sample-plugin-settings-compat` as the scoping class.
|
|
368
|
+
|
|
369
|
+
**How it works**:
|
|
370
|
+
- The CSS uses the `:has()` selector to detect if a `.setting-item` immediately follows the heading
|
|
371
|
+
- If settings exist below the heading, no border-bottom is applied (avoiding double divider)
|
|
372
|
+
- If no settings follow, border-bottom is applied for visual separation
|
|
373
|
+
- The scoping class (`{manifest-id}-settings-compat`) ensures CSS only affects headings within this plugin's settings container
|
|
374
|
+
- This only affects older builds (< 1.11.0) where the compatibility fallback is used
|
|
375
|
+
- On Obsidian 1.11.0+, `SettingGroup` handles styling automatically, so this CSS has no effect
|
|
376
|
+
|
|
377
|
+
**Note**: The `:has()` selector is well-supported in modern Obsidian (Chromium-based). If you need to support very old browsers, see the alternative TypeScript-based approach in the Common Pitfalls section below.
|
|
378
|
+
|
|
379
|
+
### How It Works
|
|
380
|
+
|
|
381
|
+
- **On Obsidian 1.11.0+**: Uses `SettingGroup` with proper styling and grouping
|
|
382
|
+
- **On older versions**: Creates a manual heading (`<h3>`) and uses regular `Setting` objects
|
|
383
|
+
- **Same API**: Your code using `addSetting()` works identically in both cases
|
|
384
|
+
|
|
385
|
+
### Common Pitfalls
|
|
386
|
+
|
|
387
|
+
#### Pitfall 1: TypeScript Errors with SettingGroup Import
|
|
388
|
+
|
|
389
|
+
**Problem**: You may see this TypeScript error:
|
|
390
|
+
```ts
|
|
391
|
+
Module '"obsidian"' has no exported member 'SettingGroup'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Cause**: `SettingGroup` may exist at runtime in Obsidian 1.11.0+ but may not be in the TypeScript type definitions, causing compile-time errors.
|
|
395
|
+
|
|
396
|
+
**Solution**: Use dynamic `require()` instead of direct import, as shown in the compatibility utility above. Do not import `SettingGroup` directly:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// ❌ WRONG - Causes TypeScript errors
|
|
400
|
+
import { SettingGroup } from 'obsidian';
|
|
401
|
+
|
|
402
|
+
// ✅ CORRECT - Use dynamic require()
|
|
403
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
404
|
+
const obsidian = require('obsidian');
|
|
405
|
+
const SettingGroup = obsidian.SettingGroup as SettingGroupConstructor;
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Pitfall 2: Missing Closing Parentheses
|
|
409
|
+
|
|
410
|
+
**Problem**: Arrow functions with method chaining need proper closing parentheses and semicolons.
|
|
411
|
+
|
|
412
|
+
**Solution**: Always include the closing parenthesis and semicolon:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
// ❌ WRONG - Missing closing parenthesis
|
|
416
|
+
generalGroup.addSetting((setting) =>
|
|
417
|
+
setting
|
|
418
|
+
.setName("Enable feature")
|
|
419
|
+
.addToggle((toggle) =>
|
|
420
|
+
toggle.setValue(this.plugin.settings.enabled)
|
|
421
|
+
)
|
|
422
|
+
// Missing closing parenthesis here!
|
|
423
|
+
|
|
424
|
+
// ✅ CORRECT - Proper closing
|
|
425
|
+
generalGroup.addSetting((setting) =>
|
|
426
|
+
setting
|
|
427
|
+
.setName("Enable feature")
|
|
428
|
+
.addToggle((toggle) =>
|
|
429
|
+
toggle.setValue(this.plugin.settings.enabled)
|
|
430
|
+
)
|
|
431
|
+
); // Closing parenthesis and semicolon required
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Pitfall 3: Storing Setting References
|
|
435
|
+
|
|
436
|
+
**Problem**: If you need to reference a `Setting` object later (e.g., for visibility toggling), you must use block syntax `{ }` instead of expression syntax.
|
|
437
|
+
|
|
438
|
+
**Solution**: Use block syntax when you need to store references:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
// ❌ WRONG - Can't store reference with expression syntax
|
|
442
|
+
let mySetting: Setting;
|
|
443
|
+
generalGroup.addSetting((setting) =>
|
|
444
|
+
setting.setName("My Setting")
|
|
445
|
+
// Can't assign: mySetting = setting; (syntax error)
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// ✅ CORRECT - Use block syntax to store reference
|
|
449
|
+
let mySetting: Setting;
|
|
450
|
+
generalGroup.addSetting((setting) => {
|
|
451
|
+
mySetting = setting; // Now we can store the reference
|
|
452
|
+
setting
|
|
453
|
+
.setName("My Setting")
|
|
454
|
+
.addToggle((toggle) =>
|
|
455
|
+
toggle.setValue(this.plugin.settings.enabled)
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Later, you can use mySetting to toggle visibility:
|
|
460
|
+
mySetting.settingEl.style.display = this.plugin.settings.enabled ? "" : "none";
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Alternative: Force Minimum Version
|
|
464
|
+
|
|
465
|
+
If you don't need to support versions before 1.11.0, you can skip the compatibility utility:
|
|
466
|
+
|
|
467
|
+
1. Set `minAppVersion: "1.11.0"` in your `manifest.json`
|
|
468
|
+
2. Use `SettingGroup` directly:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { Setting, SettingGroup } from "obsidian";
|
|
472
|
+
|
|
473
|
+
// In settings tab:
|
|
474
|
+
const group = new SettingGroup(containerEl).setHeading("My Settings");
|
|
475
|
+
group.addSetting((setting) => {
|
|
476
|
+
// ... configure setting
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Note**: Even with `minAppVersion: "1.11.0"`, you may still encounter TypeScript errors if `SettingGroup` isn't in the type definitions. In that case, you can still use the compatibility utility approach (it will always use `SettingGroup` when `requireApiVersion('1.11.0')` returns true), or use dynamic `require()` as shown in the compatibility utility.
|
|
481
|
+
|
|
482
|
+
This approach is simpler but excludes users on older Obsidian versions. The compatibility utility still works and is recommended for maximum flexibility.
|
|
483
|
+
|
|
484
|
+
## Modal with Form Input
|
|
485
|
+
|
|
486
|
+
**Source**: Based on `.ref/obsidian-plugin-docs/docs/guides/modals.md`
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { App, Modal, Notice, Setting } from "obsidian";
|
|
490
|
+
|
|
491
|
+
interface FormData {
|
|
492
|
+
name: string;
|
|
493
|
+
email: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
class FormModal extends Modal {
|
|
497
|
+
result: FormData;
|
|
498
|
+
onSubmit: (result: FormData) => void;
|
|
499
|
+
|
|
500
|
+
constructor(app: App, onSubmit: (result: FormData) => void) {
|
|
501
|
+
super(app);
|
|
502
|
+
this.onSubmit = onSubmit;
|
|
503
|
+
this.result = { name: "", email: "" };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
onOpen() {
|
|
507
|
+
const { contentEl } = this;
|
|
508
|
+
contentEl.createEl("h2", { text: "Enter Information" });
|
|
509
|
+
|
|
510
|
+
new Setting(contentEl)
|
|
511
|
+
.setName("Name")
|
|
512
|
+
.addText((text) =>
|
|
513
|
+
text.onChange((value) => {
|
|
514
|
+
this.result.name = value;
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
new Setting(contentEl)
|
|
519
|
+
.setName("Email")
|
|
520
|
+
.addText((text) =>
|
|
521
|
+
text
|
|
522
|
+
.setPlaceholder("email@example.com")
|
|
523
|
+
.onChange((value) => {
|
|
524
|
+
this.result.email = value;
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
new Setting(contentEl)
|
|
529
|
+
.addButton((btn) =>
|
|
530
|
+
btn
|
|
531
|
+
.setButtonText("Submit")
|
|
532
|
+
.setCta()
|
|
533
|
+
.onClick(() => {
|
|
534
|
+
if (!this.result.name || !this.result.email) {
|
|
535
|
+
new Notice("Please fill in all fields");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
this.close();
|
|
539
|
+
this.onSubmit(this.result);
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
onClose() {
|
|
545
|
+
const { contentEl } = this;
|
|
546
|
+
contentEl.empty();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Usage:
|
|
551
|
+
new FormModal(this.app, (result) => {
|
|
552
|
+
new Notice(`Submitted: ${result.name} (${result.email})`);
|
|
553
|
+
}).open();
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## SuggestModal Implementation
|
|
557
|
+
|
|
558
|
+
**Source**: Based on `.ref/obsidian-plugin-docs/docs/guides/modals.md`
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
import { App, Notice, SuggestModal } from "obsidian";
|
|
562
|
+
|
|
563
|
+
interface Item {
|
|
564
|
+
title: string;
|
|
565
|
+
description: string;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const ALL_ITEMS: Item[] = [
|
|
569
|
+
{ title: "Item 1", description: "Description 1" },
|
|
570
|
+
{ title: "Item 2", description: "Description 2" },
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
class ItemSuggestModal extends SuggestModal<Item> {
|
|
574
|
+
onChoose: (item: Item) => void;
|
|
575
|
+
|
|
576
|
+
constructor(app: App, onChoose: (item: Item) => void) {
|
|
577
|
+
super(app);
|
|
578
|
+
this.onChoose = onChoose;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
getSuggestions(query: string): Item[] {
|
|
582
|
+
return ALL_ITEMS.filter((item) =>
|
|
583
|
+
item.title.toLowerCase().includes(query.toLowerCase())
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
renderSuggestion(item: Item, el: HTMLElement) {
|
|
588
|
+
el.createEl("div", { text: item.title });
|
|
589
|
+
el.createEl("small", { text: item.description });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
onChooseSuggestion(item: Item, evt: MouseEvent | KeyboardEvent) {
|
|
593
|
+
this.onChoose(item);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Usage:
|
|
598
|
+
new ItemSuggestModal(this.app, (item) => {
|
|
599
|
+
new Notice(`Selected: ${item.title}`);
|
|
600
|
+
}).open();
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## Custom View with Registration
|
|
604
|
+
|
|
605
|
+
**Source**: Based on `.ref/obsidian-plugin-docs/docs/guides/custom-views.md`
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
import { ItemView, WorkspaceLeaf } from "obsidian";
|
|
609
|
+
|
|
610
|
+
export const VIEW_TYPE_MY_VIEW = "my-view";
|
|
611
|
+
|
|
612
|
+
export class MyView extends ItemView {
|
|
613
|
+
private content: string;
|
|
614
|
+
|
|
615
|
+
constructor(leaf: WorkspaceLeaf) {
|
|
616
|
+
super(leaf);
|
|
617
|
+
this.content = "Initial content";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
getViewType(): string {
|
|
621
|
+
return VIEW_TYPE_MY_VIEW;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
getDisplayText(): string {
|
|
625
|
+
return "My Custom View";
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
getIcon(): string {
|
|
629
|
+
return "document"; // Icon name
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async onOpen() {
|
|
633
|
+
const container = this.containerEl.children[1];
|
|
634
|
+
container.empty();
|
|
635
|
+
|
|
636
|
+
container.createEl("h2", { text: "My View" });
|
|
637
|
+
|
|
638
|
+
const contentEl = container.createEl("div", { cls: "my-view-content" });
|
|
639
|
+
contentEl.setText(this.content);
|
|
640
|
+
|
|
641
|
+
// Add interactive elements
|
|
642
|
+
const button = container.createEl("button", { text: "Update" });
|
|
643
|
+
button.addEventListener("click", () => {
|
|
644
|
+
this.updateContent();
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async onClose() {
|
|
649
|
+
// Clean up resources
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private updateContent() {
|
|
653
|
+
const container = this.containerEl.children[1];
|
|
654
|
+
const contentEl = container.querySelector(".my-view-content");
|
|
655
|
+
if (contentEl) {
|
|
656
|
+
this.content = "Updated content";
|
|
657
|
+
contentEl.setText(this.content);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// In main plugin class:
|
|
663
|
+
export default class MyPlugin extends Plugin {
|
|
664
|
+
async onload() {
|
|
665
|
+
// Register view
|
|
666
|
+
this.registerView(VIEW_TYPE_MY_VIEW, (leaf) => new MyView(leaf));
|
|
667
|
+
|
|
668
|
+
// Add command to open view
|
|
669
|
+
this.addCommand({
|
|
670
|
+
id: "open-my-view",
|
|
671
|
+
name: "Open My View",
|
|
672
|
+
callback: () => {
|
|
673
|
+
this.activateView();
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async activateView() {
|
|
679
|
+
const { workspace } = this.app;
|
|
680
|
+
|
|
681
|
+
let leaf = workspace.getLeavesOfType(VIEW_TYPE_MY_VIEW)[0];
|
|
682
|
+
|
|
683
|
+
if (!leaf) {
|
|
684
|
+
leaf = workspace.getRightLeaf(false);
|
|
685
|
+
await leaf.setViewState({ type: VIEW_TYPE_MY_VIEW, active: true });
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
workspace.revealLeaf(leaf);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async onunload() {
|
|
692
|
+
this.app.workspace.detachLeavesOfType(VIEW_TYPE_MY_VIEW);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## File Operations
|
|
698
|
+
|
|
699
|
+
**Source**: Based on `.ref/obsidian-api/obsidian.d.ts` (API is authoritative)
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
// Read a file
|
|
703
|
+
async readFile(file: TFile): Promise<string> {
|
|
704
|
+
return await this.app.vault.read(file);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Write to a file
|
|
708
|
+
async writeFile(file: TFile, content: string): Promise<void> {
|
|
709
|
+
await this.app.vault.modify(file, content);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Create a new file
|
|
713
|
+
async createFile(path: string, content: string): Promise<TFile> {
|
|
714
|
+
return await this.app.vault.create(path, content);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Delete a file (respects user's trash preference)
|
|
718
|
+
async deleteFile(file: TFile): Promise<void> {
|
|
719
|
+
await this.app.fileManager.trashFile(file);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Check if file exists
|
|
723
|
+
fileExists(path: string): boolean {
|
|
724
|
+
return this.app.vault.getAbstractFileByPath(path) !== null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Get all markdown files
|
|
728
|
+
getAllMarkdownFiles(): TFile[] {
|
|
729
|
+
return this.app.vault.getMarkdownFiles();
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
## Workspace Events
|
|
734
|
+
|
|
735
|
+
**Source**: Based on `.ref/obsidian-api/obsidian.d.ts` and `.ref/obsidian-sample-plugin/main.ts`
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
// File opened event
|
|
739
|
+
this.registerEvent(
|
|
740
|
+
this.app.workspace.on("file-open", (file) => {
|
|
741
|
+
if (file) {
|
|
742
|
+
console.log("File opened:", file.path);
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
// Active leaf changed
|
|
748
|
+
this.registerEvent(
|
|
749
|
+
this.app.workspace.on("active-leaf-change", (leaf) => {
|
|
750
|
+
if (leaf?.view instanceof MarkdownView) {
|
|
751
|
+
console.log("Active markdown view:", leaf.view.file?.path);
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
// Layout changed
|
|
757
|
+
this.registerEvent(
|
|
758
|
+
this.app.workspace.on("layout-change", () => {
|
|
759
|
+
console.log("Workspace layout changed");
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
// Editor change (in markdown view)
|
|
764
|
+
this.registerEvent(
|
|
765
|
+
this.app.workspace.on("editor-change", (editor, info) => {
|
|
766
|
+
console.log("Editor changed:", info);
|
|
767
|
+
})
|
|
768
|
+
);
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
## Status Bar with Updates
|
|
772
|
+
|
|
773
|
+
**Source**: Based on `.ref/obsidian-sample-plugin/main.ts` and `.ref/obsidian-plugin-docs/docs/guides/status-bar.md`
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
export default class MyPlugin extends Plugin {
|
|
777
|
+
private statusBarItem: HTMLElement;
|
|
778
|
+
|
|
779
|
+
async onload() {
|
|
780
|
+
// Create status bar item
|
|
781
|
+
this.statusBarItem = this.addStatusBarItem();
|
|
782
|
+
this.updateStatusBar("Ready");
|
|
783
|
+
|
|
784
|
+
// Update status bar periodically
|
|
785
|
+
this.registerInterval(
|
|
786
|
+
window.setInterval(() => {
|
|
787
|
+
this.updateStatusBar(`Time: ${new Date().toLocaleTimeString()}`);
|
|
788
|
+
}, 1000)
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Update on file open
|
|
792
|
+
this.registerEvent(
|
|
793
|
+
this.app.workspace.on("file-open", (file) => {
|
|
794
|
+
if (file) {
|
|
795
|
+
this.updateStatusBar(`Open: ${file.name}`);
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private updateStatusBar(text: string) {
|
|
802
|
+
this.statusBarItem.empty();
|
|
803
|
+
this.statusBarItem.createEl("span", { text });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
## Editor Interactions
|
|
809
|
+
|
|
810
|
+
**Source**: Based on `.ref/obsidian-sample-plugin/main.ts` and `.ref/obsidian-api/obsidian.d.ts`
|
|
811
|
+
|
|
812
|
+
```ts
|
|
813
|
+
// Get active editor
|
|
814
|
+
getActiveEditor(): Editor | null {
|
|
815
|
+
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
816
|
+
return view?.editor ?? null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Get selected text
|
|
820
|
+
getSelection(): string {
|
|
821
|
+
const editor = this.getActiveEditor();
|
|
822
|
+
return editor?.getSelection() ?? "";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Replace selection
|
|
826
|
+
replaceSelection(text: string) {
|
|
827
|
+
const editor = this.getActiveEditor();
|
|
828
|
+
if (editor) {
|
|
829
|
+
editor.replaceSelection(text);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Insert at cursor
|
|
834
|
+
insertAtCursor(text: string) {
|
|
835
|
+
const editor = this.getActiveEditor();
|
|
836
|
+
if (editor) {
|
|
837
|
+
const cursor = editor.getCursor();
|
|
838
|
+
editor.replaceRange(text, cursor);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Get current line
|
|
843
|
+
getCurrentLine(): string {
|
|
844
|
+
const editor = this.getActiveEditor();
|
|
845
|
+
if (editor) {
|
|
846
|
+
const line = editor.getCursor().line;
|
|
847
|
+
return editor.getLine(line);
|
|
848
|
+
}
|
|
849
|
+
return "";
|
|
850
|
+
}
|
|
851
|
+
```
|
|
852
|
+
|