Getting Started
This guide will walk you through creating your first Lokus plugin from scratch. By the end, you’ll have a working plugin that adds a custom command and UI panel to Lokus.
Prerequisites
Before you begin, ensure you have:
- Node.js 18+ installed
- npm or yarn package manager
- Lokus installed on your system
- Basic knowledge of TypeScript and JavaScript
- A code editor (VS Code recommended)
Quick Start
The fastest way to create a new plugin is using the Lokus Plugin CLI:
# Install the CLI globally
npm install -g @lokus/plugin-cli
# Create a new plugin
lokus-plugin create my-first-plugin
# Navigate to the plugin directory
cd my-first-plugin
# Start development mode
lokus-plugin dev
This creates a fully functional plugin with:
- TypeScript configuration
- Plugin manifest
- Example code
- Test setup
- Hot reload support
Project Structure
Let’s explore the generated project structure:
my-first-plugin/
├── src/
│ ├── index.ts # Plugin entry point
│ ├── commands.ts # Command definitions
│ └── panels.ts # UI panel definitions
├── assets/
│ ├── icon.png # Plugin icon
│ └── logo.svg # Logo assets
├── test/
│ └── index.test.ts # Unit tests
├── package.json # npm package configuration
├── plugin.json # Plugin manifest
├── tsconfig.json # TypeScript configuration
├── README.md # Plugin documentation
└── .gitignore # Git ignore rules
Understanding the Manifest
The plugin.json
file is the heart of your plugin. It defines metadata, permissions, and contribution points:
{
"id": "my-first-plugin",
"version": "0.1.0",
"name": "My First Plugin",
"description": "A simple example plugin for Lokus",
"author": "Your Name",
"license": "MIT",
"lokusVersion": "^1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"activationEvents": [
"onStartup"
],
"permissions": [
"editor:read",
"ui:create",
"commands:register"
],
"contributes": {
"commands": [
{
"command": "myFirstPlugin.hello",
"title": "Say Hello",
"category": "My Plugin"
}
]
}
}
Creating Your First Plugin
Let’s build a simple plugin step by step.
Step 1: Define the Plugin Class
Create or edit src/index.ts
:
import { Plugin, PluginContext } from '@lokus/plugin-sdk'
export default class MyFirstPlugin implements Plugin {
private context?: PluginContext
async activate(context: PluginContext) {
this.context = context
console.log('My First Plugin is now active!')
// Register a command
this.registerCommands()
// Create a UI panel
this.createPanel()
}
async deactivate() {
console.log('My First Plugin is deactivated')
// Cleanup code here
}
private registerCommands() {
if (!this.context) return
// Register "Say Hello" command
this.context.subscriptions.push(
this.context.api.commands.register({
id: 'myFirstPlugin.hello',
title: 'Say Hello',
handler: () => this.sayHello()
})
)
}
private sayHello() {
if (!this.context) return
this.context.api.ui.showNotification(
'Hello from My First Plugin!',
'success'
)
}
private createPanel() {
if (!this.context) return
this.context.subscriptions.push(
this.context.api.ui.registerPanel({
id: 'myFirstPlugin.panel',
title: 'My First Panel',
type: 'webview',
location: 'sidebar',
icon: 'star',
html: this.getPanelHTML()
})
)
}
private getPanelHTML(): string {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body {
padding: 20px;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
color: #007acc;
margin-bottom: 20px;
}
.button {
background: #007acc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.button:hover {
background: #005a9e;
}
</style>
</head>
<body>
<h1>Welcome!</h1>
<p>This is your first Lokus plugin panel.</p>
<button class="button" onclick="handleClick()">
Click Me
</button>
<script>
function handleClick() {
// Send message to plugin host
window.parent.postMessage({
command: 'buttonClicked'
}, '*')
}
</script>
</body>
</html>
`
}
}
Step 2: Add Package Dependencies
Edit package.json
:
{
"name": "my-first-plugin",
"version": "0.1.0",
"description": "My first Lokus plugin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "lokus-plugin dev",
"test": "jest",
"lint": "eslint src --ext .ts",
"package": "lokus-plugin package"
},
"dependencies": {
"@lokus/plugin-sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"ts-jest": "^29.0.0"
},
"lokus": {
"manifest": "./plugin.json"
}
}
Step 3: Configure TypeScript
Create or edit tsconfig.json
:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
Step 4: Build and Test
# Install dependencies
npm install
# Build the plugin
npm run build
# Run in development mode with hot reload
npm run dev
Testing Your Plugin
Manual Testing
-
Start Development Mode:
lokus-plugin dev
-
Open Lokus - The plugin should be automatically loaded
-
Test the Command:
- Open command palette (Ctrl/Cmd + Shift + P)
- Type “Say Hello”
- Execute the command
- You should see a notification
-
Test the Panel:
- Look for “My First Panel” in the sidebar
- Click to open it
- Interact with the button
Automated Testing
Create test/index.test.ts
:
import { describe, it, expect, beforeEach } from '@jest/globals'
import { createMockContext, TestHelper } from '@lokus/plugin-sdk/testing'
import MyFirstPlugin from '../src/index'
describe('MyFirstPlugin', () => {
let plugin: MyFirstPlugin
let context: ReturnType<typeof createMockContext>
let helper: TestHelper
beforeEach(() => {
context = createMockContext()
helper = new TestHelper(context)
plugin = new MyFirstPlugin()
})
it('should activate successfully', async () => {
await plugin.activate(context)
expect(context.subscriptions.length).toBeGreaterThan(0)
})
it('should register hello command', async () => {
await plugin.activate(context)
const commands = await helper.getRegisteredCommands()
expect(commands).toContain('myFirstPlugin.hello')
})
it('should create panel', async () => {
await plugin.activate(context)
const panels = await helper.getRegisteredPanels()
expect(panels).toContain('myFirstPlugin.panel')
})
it('should show notification when command executed', async () => {
await plugin.activate(context)
await context.api.commands.execute('myFirstPlugin.hello')
const notifications = helper.getNotifications()
expect(notifications.length).toBe(1)
expect(notifications[0].message).toBe('Hello from My First Plugin!')
expect(notifications[0].type).toBe('success')
})
it('should deactivate cleanly', async () => {
await plugin.activate(context)
await plugin.deactivate()
// Verify cleanup
expect(context.subscriptions.every(s => s.disposed)).toBe(true)
})
})
Run tests:
npm test
Adding More Features
Adding a Second Command
private registerCommands() {
if (!this.context) return
// Existing hello command
this.context.subscriptions.push(
this.context.api.commands.register({
id: 'myFirstPlugin.hello',
title: 'Say Hello',
handler: () => this.sayHello()
})
)
// New command: Insert current date
this.context.subscriptions.push(
this.context.api.commands.register({
id: 'myFirstPlugin.insertDate',
title: 'Insert Current Date',
handler: () => this.insertDate()
})
)
}
private async insertDate() {
if (!this.context) return
const date = new Date().toLocaleDateString()
await this.context.api.editor.insertContent(date)
}
Don’t forget to add the command to your manifest:
{
"contributes": {
"commands": [
{
"command": "myFirstPlugin.hello",
"title": "Say Hello",
"category": "My Plugin"
},
{
"command": "myFirstPlugin.insertDate",
"title": "Insert Current Date",
"category": "My Plugin"
}
]
}
}
Adding a Keyboard Shortcut
Add to your manifest:
{
"contributes": {
"keybindings": [
{
"command": "myFirstPlugin.insertDate",
"key": "ctrl+shift+d",
"mac": "cmd+shift+d",
"when": "editorTextFocus"
}
]
}
}
Adding Configuration
Add to your manifest:
{
"contributes": {
"configuration": {
"title": "My First Plugin",
"properties": {
"myFirstPlugin.greeting": {
"type": "string",
"default": "Hello",
"description": "Greeting message to display"
},
"myFirstPlugin.showTimestamp": {
"type": "boolean",
"default": true,
"description": "Show timestamp with greeting"
}
}
}
}
}
Use configuration in your code:
private async sayHello() {
if (!this.context) return
const greeting = await this.context.api.config.get<string>(
'myFirstPlugin.greeting',
'Hello'
)
const showTimestamp = await this.context.api.config.get<boolean>(
'myFirstPlugin.showTimestamp',
true
)
let message = `${greeting} from My First Plugin!`
if (showTimestamp) {
const time = new Date().toLocaleTimeString()
message += ` (${time})`
}
this.context.api.ui.showNotification(message, 'success')
}
Debugging
Enable Debug Mode
Edit plugin.json
:
{
"dev": {
"hotReload": true,
"debug": true,
"sourceMaps": true,
"verboseLogging": true
}
}
Using Debug Logs
export default class MyFirstPlugin implements Plugin {
async activate(context: PluginContext) {
// Debug logging
context.api.log('debug', 'Plugin activating...')
try {
this.registerCommands()
context.api.log('info', 'Commands registered successfully')
} catch (error) {
context.api.log('error', 'Failed to register commands', error)
}
}
}
Using Breakpoints
- Open Lokus in development mode
- Open Developer Tools (View → Developer → Toggle Developer Tools)
- Navigate to Sources tab
- Find your plugin code
- Set breakpoints
- Trigger plugin functionality
Packaging
When you’re ready to share your plugin:
# Package the plugin
lokus-plugin package
# This creates: my-first-plugin-0.1.0.vsix
The .vsix
file can be:
- Installed locally in Lokus
- Shared with others
- Published to the plugin registry
Common Patterns
Lazy Loading
Load heavy dependencies only when needed:
private async processMarkdown() {
// Lazy load markdown-it
const { default: MarkdownIt } = await import('markdown-it')
const md = new MarkdownIt()
return md.render(content)
}
Error Handling
Always handle errors gracefully:
private async safeExecute<T>(
operation: () => Promise<T>,
fallback: T
): Promise<T> {
try {
return await operation()
} catch (error) {
this.context?.api.log('error', 'Operation failed', error)
this.context?.api.ui.showNotification(
'Operation failed. Please try again.',
'error'
)
return fallback
}
}
Resource Cleanup
Always clean up resources in deactivate:
export default class MyFirstPlugin implements Plugin {
private timers: NodeJS.Timeout[] = []
private watchers: Disposable[] = []
async activate(context: PluginContext) {
// Create resources
this.timers.push(
setInterval(() => this.checkUpdates(), 60000)
)
const watcher = context.api.fs.watch('/path', () => {})
this.watchers.push(watcher)
}
async deactivate() {
// Clean up timers
this.timers.forEach(timer => clearInterval(timer))
this.timers = []
// Dispose watchers
this.watchers.forEach(watcher => watcher.dispose())
this.watchers = []
}
}
Troubleshooting
Plugin Won’t Activate
Check:
- Manifest file is valid JSON
- Required permissions are declared
- Activation events are properly configured
- No syntax errors in code
Solution:
# Validate plugin
lokus-plugin validate
# Check logs
lokus-plugin dev --verbose
Hot Reload Not Working
Check:
- Development mode is enabled
- File watcher is running
- No compilation errors
Solution:
# Restart dev mode
lokus-plugin dev --clean
Commands Not Appearing
Check:
- Commands declared in manifest
- Command IDs match between manifest and code
- Command registration in activate method
Solution: Verify manifest and code match:
// plugin.json
{
"contributes": {
"commands": [
{ "command": "myPlugin.hello", "title": "Say Hello" }
]
}
}
// index.ts
api.commands.register({
id: 'myPlugin.hello', // Must match manifest
title: 'Say Hello',
handler: () => {}
})
Next Steps
Now that you have a working plugin, explore more advanced topics:
Related Documentation: