feat: milestones 6b-fix through 6e — default-editor, files plugin, workspace host, workspaceItems contribution
- Fix PluginCard openProviders display - Add default-editor plugin (text/markdown/notes-context) - Add files plugin with workspaceItems placement - Add workspaceItems contribution point (Go + API + mock + SDK) - Add WorkspaceHost component for workspace area - WorkspaceTree dispatches selection event - Fix default-editor layout to fill container - Fix PluginCard unsafe .length access - Add E2E tests: 34/34 pass - Add bundle execution check to official-plugins check.sh - Update docs: PLUGIN_RUNTIME, DEV_PLUGINS, MILESTONE_6B/6C/6D plans
This commit is contained in:
parent
6ed6df311a
commit
a6412fa070
|
|
@ -101,6 +101,12 @@ Workbench selects by resource kind, extension/mime, context (`generic-text`,
|
|||
deterministic `pluginId/providerId` tie-break. If nothing matches, Workbench
|
||||
shows `no-provider` fallback instead of a core editor.
|
||||
|
||||
The official `verstak.default-editor` plugin provides three openProviders:
|
||||
`verstak.default-editor.text` (generic-text), `verstak.default-editor.markdown`
|
||||
(generic-markdown), and `verstak.default-editor.notes-markdown` (notes-context).
|
||||
It uses a single unified `DefaultEditor` component with textarea-based editing,
|
||||
simple markdown preview, dirty state tracking, and Ctrl+S save.
|
||||
|
||||
This is a cooperative contract, not a sandbox. Bundled plugins run in the same JS
|
||||
context as the desktop frontend; real isolation is deferred to the sidecar/sandbox
|
||||
milestone.
|
||||
|
|
|
|||
|
|
@ -133,3 +133,16 @@ Out of scope:
|
|||
- Watcher/sync/binary streaming/external editor.
|
||||
- Sidecar/security boundary.
|
||||
- Large rewrite.
|
||||
|
||||
## 6b-fix: Infrastructure Gaps Closed
|
||||
|
||||
Post-review fixes before Milestone 6c:
|
||||
|
||||
- Added `openProviders` to `verstak-sdk/schemas/contributions.json` contribution points.
|
||||
- Added provider matching tests: text/markdown preference, MIME matching, extension
|
||||
case-insensitivity, multiple supports entries, kind mismatch.
|
||||
- Added disabled provider exclusion test in `api/app_test.go`.
|
||||
- Fixed SDK `build.sh`/`test.sh` to detect incomplete `node_modules`.
|
||||
- Documented disabled provider lifecycle: contributions remain in registry until
|
||||
ReloadPlugins; `activeOpenProviders()` filters disabled/unloaded at request time.
|
||||
- Installed Playwright browsers for E2E tests.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# Milestone 6c — Default Editor Plugin
|
||||
|
||||
## Goal
|
||||
|
||||
Create the official Default Editor Plugin as an openProvider for text, generic
|
||||
markdown, and notes-context markdown files. Core desktop does not own or import
|
||||
the editor component.
|
||||
|
||||
## What was built
|
||||
|
||||
### Plugin: `verstak.default-editor`
|
||||
|
||||
Location: `verstak-official-plugins/plugins/default-editor/`
|
||||
|
||||
**Manifest declares 3 openProviders:**
|
||||
|
||||
| Provider ID | Context | Extensions |
|
||||
|-------------|---------|------------|
|
||||
| `verstak.default-editor.text` | `generic-text` | `.txt`, `.log`, `.conf`, `.ini`, `.toml`, `.yaml`, `.yml`, `.json`, `.csv` |
|
||||
| `verstak.default-editor.markdown` | `generic-markdown` | `.md`, `.markdown` |
|
||||
| `verstak.default-editor.notes-markdown` | `notes-markdown` | `.md`, `.markdown` |
|
||||
|
||||
All providers use the same `DefaultEditor` component (unified, not 3 separate editors).
|
||||
|
||||
**Permissions:** `files.read`, `files.write`, `workbench.open`
|
||||
|
||||
**Capabilities required:** `verstak/core/files/v1`, `verstak/core/workbench/v1`
|
||||
|
||||
### Frontend component: `DefaultEditor`
|
||||
|
||||
- **Modes:** text (textarea), generic-markdown (editor + preview), notes-markdown (editor + preview + notes badge)
|
||||
- **File loading:** `api.files.readText(path)` with loading/error states
|
||||
- **Saving:** `api.files.writeText(path, content, { overwrite: true })` with dirty/saved/error states
|
||||
- **Keyboard:** Ctrl+S / Cmd+S save, Tab indentation
|
||||
- **Markdown preview:** Simple renderer (no raw HTML, no script injection)
|
||||
- **Notes context:** Badge + info bar, no separate note entity, no `.verstak/notes`
|
||||
|
||||
## Verification
|
||||
|
||||
- `go test ./...` — PASS
|
||||
- `go vet ./...` — PASS
|
||||
- `npm run build` (frontend) — PASS
|
||||
- `npm run test:e2e` — 28/28 PASS (20 existing + 8 new)
|
||||
- `scripts/check.sh` (official-plugins) — PASS
|
||||
- `scripts/build.sh` (official-plugins) — PASS
|
||||
- SDK checks — PASS
|
||||
|
||||
## Manual testing
|
||||
|
||||
Until Files UI plugin exists, use platform-test diagnostics panel to manually
|
||||
open files via workbench: click "Open Text Diagnostic", "Open Markdown Diagnostic",
|
||||
or "Open Notes Diagnostic" buttons. These call `api.workbench.editResource` and
|
||||
route through the default-editor provider.
|
||||
|
||||
## Deferred
|
||||
|
||||
- CodeMirror/Monaco editor
|
||||
- Backlinks, internal link navigation
|
||||
- Secret widgets
|
||||
- Image asset pipeline
|
||||
- Files UI plugin
|
||||
- Notes UI plugin
|
||||
- Watcher/sync
|
||||
- External open
|
||||
- Sidecar/security isolation
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Milestone 6d — Minimal Files Plugin
|
||||
|
||||
## Goal
|
||||
|
||||
Create a minimal Files plugin that shows vault files/folders and opens files
|
||||
through Workbench openResource. No editor embedded.
|
||||
|
||||
## What was built
|
||||
|
||||
### Plugin: `verstak.files`
|
||||
|
||||
Location: `verstak-official-plugins/plugins/files/`
|
||||
|
||||
**Contributions:**
|
||||
- views: `verstak.files.view` → `FilesView` component
|
||||
- No sidebarItems — Files is not a global sidebar item
|
||||
|
||||
**Permissions:** `files.read`, `files.write`, `workbench.open`, `ui.register`
|
||||
|
||||
### Files View
|
||||
|
||||
- Root listing on mount
|
||||
- Folder navigation (double-click)
|
||||
- File open via `api.workbench.openResource()`
|
||||
- Breadcrumb navigation
|
||||
- Create folder/file buttons
|
||||
- Refresh button
|
||||
- Loading/error/empty states
|
||||
- `.verstak` filtered out
|
||||
|
||||
### Provider priority
|
||||
|
||||
- default-editor: priority 50
|
||||
- platform-test diagnostic: priority 10
|
||||
- default-editor wins for normal file opens
|
||||
|
||||
### Bundle fix
|
||||
|
||||
Fixed missing opening quote in STYLES string (`.files-empty` → `'.files-empty'`).
|
||||
Added automated bundle execution check to `scripts/check.sh`.
|
||||
|
||||
## 6d-hotfix
|
||||
|
||||
- Removed sidebarItems from Files plugin (Files is not a global sidebar item)
|
||||
- Added `[frontend bundle execution]` check to `check.sh` — verifies all plugin
|
||||
bundles parse via `new Function()` and register via `VerstakPluginRegister`
|
||||
- Updated E2E tests: Files no longer expected in global sidebar
|
||||
- Documented: sidebarItems are global shell navigation, not workspace template tabs
|
||||
|
||||
## Verification
|
||||
|
||||
- `go test ./...` — PASS
|
||||
- `go vet ./...` — PASS
|
||||
- `npm run build` — PASS
|
||||
- `npm run test:e2e` — 34/34 PASS
|
||||
- Official plugins — 3 plugins built, bundle execution check passes
|
||||
- SDK — 11/11 tests pass
|
||||
|
||||
## Deferred
|
||||
|
||||
- Notes plugin, rename/move/trash UI, drag-and-drop, context menu,
|
||||
watcher/inotify, sync, external open, binary streaming, sidecar/security,
|
||||
workspace template host (Milestone 6d2)
|
||||
|
||||
|
|
@ -335,10 +335,27 @@ extension/mime, context, user preference, priority, then deterministic
|
|||
`pluginId/providerId` fallback. If nothing matches, Workbench returns
|
||||
`status: "no-provider"` and shows the fallback view instead of a core editor.
|
||||
|
||||
Disabled/failed/missing-required-capability plugins are excluded from provider
|
||||
selection at request time by `activeOpenProviders()`. Their contributions may
|
||||
remain in the registry until the next `ReloadPlugins()` cycle, but they never
|
||||
match during routing.
|
||||
|
||||
Draft app-global preferences are `defaultTextEditorProvider`,
|
||||
`defaultMarkdownEditorProvider`, and `defaultNotesMarkdownEditorProvider`.
|
||||
Vault-scoped and per-extension overrides are deferred.
|
||||
|
||||
### Default Editor Plugin
|
||||
|
||||
The official `verstak.default-editor` plugin (`verstak-official-plugins/plugins/default-editor/`)
|
||||
provides openProviders for text, generic markdown, and notes-context markdown files.
|
||||
It uses `api.files.readText` / `api.files.writeText` for file I/O and mounts through
|
||||
the standard `PluginBundleHost` / provider host mechanism. Core does not import or
|
||||
reference this plugin directly.
|
||||
|
||||
Provider plugins may have no sidebar item — openProviders are contribution points
|
||||
for workbench routing, not navigation. Plugin Manager displays openProviders in the
|
||||
contributions summary.
|
||||
|
||||
### API methods
|
||||
|
||||
`settings`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('F: Default Editor Plugin', () => {
|
||||
let consoleCollector;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
consoleCollector = setupConsoleCollector(page);
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
consoleCollector.assertNoErrors();
|
||||
});
|
||||
|
||||
test('open .txt file shows plain text editor mode', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const editor = page.locator('[data-editor-mode="text"]');
|
||||
await expect(editor).toBeVisible({ timeout: 10000 });
|
||||
await expect(editor).toHaveAttribute('data-resource-path', 'Docs/todo.txt');
|
||||
await expect(editor).toHaveAttribute('data-request-mode', 'view');
|
||||
const textarea = editor.locator('[data-editor-textarea]');
|
||||
await expect(textarea).toBeVisible();
|
||||
await expect(textarea).toHaveValue('Buy groceries\nWrite tests');
|
||||
});
|
||||
|
||||
test('open .md file outside Notes routes to highest-priority provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/readme.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Docs/readme.md');
|
||||
});
|
||||
|
||||
test('open .md with notes context routes to highest-priority provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Notes/Overview.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'notes', isInsideNotesFolder: true, notesMode: true },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Notes/Overview.md');
|
||||
});
|
||||
|
||||
test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.status-badge')).toHaveText('loaded');
|
||||
});
|
||||
|
||||
test('disable default-editor plugin removes its providers', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await card.locator('button.btn-disable').click();
|
||||
await expect(card.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-workbench-status="no-provider"]')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('default-editor plugin card shows openProviders contribution count', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.meta-row').filter({ hasText: 'Contributions:' })).toContainText('3 openProviders');
|
||||
});
|
||||
|
||||
test('default-editor does not add sidebar item', async ({ page }) => {
|
||||
const sidebarItems = page.locator('.sidebar .plugin-item');
|
||||
const count = await sidebarItems.count();
|
||||
const hasDefaultEditor = await page.locator('.sidebar .plugin-item').filter({ hasText: /default.editor/i }).count();
|
||||
expect(hasDefaultEditor).toBe(0);
|
||||
});
|
||||
|
||||
test('platform-test workbench buttons open files via default-editor', async ({ page }) => {
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const editor = page.locator('[data-editor-mode="text"]');
|
||||
await expect(editor).toBeVisible({ timeout: 10000 });
|
||||
await expect(editor).toHaveAttribute('data-resource-path', 'Docs/todo.txt');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('G: Files Plugin', () => {
|
||||
let consoleCollector;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
consoleCollector = setupConsoleCollector(page);
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
consoleCollector.assertNoErrors();
|
||||
});
|
||||
|
||||
test('files plugin appears in Plugin Manager as loaded', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.files' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.status-badge')).toHaveText('loaded');
|
||||
});
|
||||
|
||||
test('files plugin does not add global sidebar item', async ({ page }) => {
|
||||
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Files' });
|
||||
await expect(sidebarItem).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('open .txt via workbench from files context shows default-editor', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const editor = page.locator('[data-editor-mode="text"]');
|
||||
await expect(editor).toBeVisible({ timeout: 10000 });
|
||||
await expect(editor).toHaveAttribute('data-resource-path', 'Docs/todo.txt');
|
||||
});
|
||||
|
||||
test('open .md via workbench from files context shows generic-markdown', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/readme.md',
|
||||
extension: '.md',
|
||||
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
await expect(workbench.locator('.workbench-title')).toHaveText('Docs/readme.md');
|
||||
});
|
||||
|
||||
test('open notes markdown via workbench from files context shows notes-markdown', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
||||
kind: 'vault-file',
|
||||
path: 'Notes/Overview.md',
|
||||
extension: '.md',
|
||||
context: { sourcePluginId: 'verstak.files', sourceView: 'files', isInsideNotesFolder: true, notesMode: true },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
await expect(workbench.locator('.workbench-title')).toHaveText('Notes/Overview.md');
|
||||
});
|
||||
|
||||
test('files plugin card shows openProviders in contributions', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
||||
kind: 'vault-file', path: 'test.txt', extension: '.txt',
|
||||
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
const editor = page.locator('[data-editor-mode="text"]');
|
||||
await expect(editor).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
|
@ -33,6 +33,7 @@ test.describe('D: Plugin API bridge', () => {
|
|||
await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).locator('.status-badge')).toHaveText('loaded', { timeout: 10000 });
|
||||
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
|
||||
await expect(page.locator('.pt-saved-setting')).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 });
|
||||
await expect(page.locator('.pt-badge')).toHaveAttribute('data-command-status', 'handled');
|
||||
await expect(page.locator('.pt-badge')).toContainText('capability available');
|
||||
|
|
@ -44,28 +45,38 @@ test.describe('D: Plugin API bridge', () => {
|
|||
await expect(page.locator('.pt-files-error-result')).toHaveAttribute('data-files-error-status', 'expected');
|
||||
await expect(page.locator('.pt-files-error-result')).toContainText('Files error path: rejected reserved-path');
|
||||
|
||||
await page.locator('.pt-open-workbench-notes').click();
|
||||
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-workbench-status', 'ok');
|
||||
await expect(page.locator('.pt-workbench-result')).toContainText('Workbench: opened Notes/Overview.md with verstak.platform-test.markdown-diagnostic');
|
||||
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-resource-context', 'notes-markdown');
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Notes/Overview.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'notes', isInsideNotesFolder: true, notesMode: true },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
await expect(workbench.locator('.workbench-title')).toHaveText('Notes/Overview.md');
|
||||
});
|
||||
|
||||
test('platform-test diagnostic provider routes text, markdown, and notes markdown contexts', async ({ page }) => {
|
||||
async function openFromDiagnostics(buttonClass, path, context) {
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
|
||||
await page.locator(buttonClass).click();
|
||||
const result = page.locator('.pt-workbench-result');
|
||||
await expect(result).toHaveAttribute('data-workbench-status', 'ok', { timeout: 10000 });
|
||||
await expect(result).toHaveAttribute('data-resource-path', path);
|
||||
await expect(result).toHaveAttribute('data-resource-mode', 'edit');
|
||||
await expect(result).toHaveAttribute('data-resource-context', context);
|
||||
await expect(result).toContainText('verstak.platform-test.markdown-diagnostic');
|
||||
}
|
||||
test('workbench routes markdown files to default-editor provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/readme.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
await openFromDiagnostics('.pt-open-workbench-text', 'Docs/todo.txt', 'generic-text');
|
||||
await openFromDiagnostics('.pt-open-workbench-markdown', 'Docs/readme.md', 'generic-markdown');
|
||||
await openFromDiagnostics('.pt-open-workbench-notes', 'Notes/Overview.md', 'notes-markdown');
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Docs/readme.md');
|
||||
});
|
||||
|
||||
test('workbench shows no-provider fallback when no provider matches', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ test.describe('E: Plugin Manager layout', () => {
|
|||
test('plugin list scrolls through the global main scroll surface and stays responsive', async ({ page }) => {
|
||||
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(18));
|
||||
await page.locator('button.reload-btn').click();
|
||||
await expect(page.locator('.plugin-card')).toHaveCount(19, { timeout: 10000 });
|
||||
await expect(page.locator('.plugin-card')).toHaveCount(21, { timeout: 10000 });
|
||||
|
||||
const manager = page.locator('.plugin-manager');
|
||||
const scrollSurface = page.locator('.content.scroll-surface');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import ViewContainer from './lib/shell/ViewContainer.svelte';
|
||||
import VaultSelection from './lib/shell/VaultSelection.svelte';
|
||||
import WorkbenchHost from './lib/shell/WorkbenchHost.svelte';
|
||||
import WorkspaceHost from './lib/shell/WorkspaceHost.svelte';
|
||||
import * as App from '../wailsjs/go/api/App';
|
||||
import { debug } from './lib/log/debug.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -20,6 +21,9 @@
|
|||
let activeSettingsPanelId = '';
|
||||
let openedResource = null;
|
||||
|
||||
let workspaceNodes = [];
|
||||
let currentWorkspaceNodeId = '';
|
||||
|
||||
function flog(msg) {
|
||||
App.WriteFrontendLog('App', msg);
|
||||
}
|
||||
|
|
@ -91,6 +95,15 @@
|
|||
currentView = 'workbench';
|
||||
}
|
||||
|
||||
function onWorkspaceNodeSelected(e) {
|
||||
debug.log('[App] onWorkspaceNodeSelected:', e.detail?.nodeId);
|
||||
currentWorkspaceNodeId = e.detail?.nodeId || '';
|
||||
workspaceNodes = e.detail?.nodes || workspaceNodes;
|
||||
if (currentWorkspaceNodeId) {
|
||||
currentView = 'workspace';
|
||||
}
|
||||
}
|
||||
|
||||
function onCloseSettings() {
|
||||
debug.log('[App] onCloseSettings');
|
||||
activeSettingsPluginId = '';
|
||||
|
|
@ -105,6 +118,7 @@
|
|||
window.addEventListener('verstak:open-settings', onOpenSettings);
|
||||
window.addEventListener('verstak:close-settings', onCloseSettings);
|
||||
window.addEventListener('verstak:workbench-opened', onWorkbenchOpened);
|
||||
window.addEventListener('verstak:workspace-node-selected', onWorkspaceNodeSelected);
|
||||
}
|
||||
|
||||
onMount(() => { checkVault(); });
|
||||
|
|
@ -125,6 +139,8 @@
|
|||
<PluginManager {activeSettingsPluginId} {activeSettingsPanelId} />
|
||||
{:else if currentView === 'workbench'}
|
||||
<WorkbenchHost {openedResource} />
|
||||
{:else if currentView === 'workspace'}
|
||||
<WorkspaceHost currentNodeId={currentWorkspaceNodeId} nodes={workspaceNodes} />
|
||||
{:else}
|
||||
<ViewContainer {activeView} {activeViewPluginId} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
commands: (contributions.commands || []).filter(c => c.pluginId === pluginId).length,
|
||||
sidebar: (contributions.sidebarItems || []).filter(s => s.pluginId === pluginId).length,
|
||||
statusbar: (contributions.statusBarItems || []).filter(s => s.pluginId === pluginId).length,
|
||||
openProviders: (contributions.openProviders || []).filter(o => o.pluginId === pluginId).length,
|
||||
};
|
||||
|
||||
$: contribSummary = (() => {
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
if (contribCounts.commands > 0) parts.push(contribCounts.commands + ' command' + (contribCounts.commands !== 1 ? 's' : ''));
|
||||
if (contribCounts.sidebar > 0) parts.push(contribCounts.sidebar + ' sidebar' + (contribCounts.sidebar !== 1 ? 's' : ''));
|
||||
if (contribCounts.statusbar > 0) parts.push(contribCounts.statusbar + ' statusbar' + (contribCounts.statusbar !== 1 ? 's' : ''));
|
||||
if (contribCounts.openProviders > 0) parts.push(contribCounts.openProviders + ' openProvider' + (contribCounts.openProviders !== 1 ? 's' : ''));
|
||||
return parts.length > 0 ? parts.join(', ') : 'none';
|
||||
})();
|
||||
|
||||
|
|
@ -200,7 +202,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Permission warnings -->
|
||||
{#if !hasUIPermission && (m.contributes && (m.contributes.views || m.contributes.sidebarItems || m.contributes.settingsPanels).length > 0)}
|
||||
{#if !hasUIPermission && m.contributes && ((m.contributes.views || []).length > 0 || (m.contributes.sidebarItems || []).length > 0 || (m.contributes.settingsPanels || []).length > 0)}
|
||||
<p class="warning"><Icon name="warning" size={12} /> Plugin has UI contributions but lacks ui.register permission</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,11 @@
|
|||
.workbench-content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.workbench-empty {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
<script>
|
||||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||
import * as App from '../../../wailsjs/go/api/App';
|
||||
|
||||
export let currentNodeId = '';
|
||||
export let nodes = [];
|
||||
|
||||
let contributions = {};
|
||||
let plugins = [];
|
||||
let workspaceTools = [];
|
||||
|
||||
$: currentNode = nodes.find(n => n.id === currentNodeId) || null;
|
||||
$: if (currentNodeId) loadTools();
|
||||
|
||||
async function loadTools() {
|
||||
try {
|
||||
const [c, p] = await Promise.all([
|
||||
App.GetContributions().catch(() => ({})),
|
||||
App.GetPlugins().catch(() => []),
|
||||
]);
|
||||
contributions = c || {};
|
||||
plugins = p || [];
|
||||
|
||||
const enabledIds = new Set(
|
||||
plugins.filter(pl => pl.enabled && (pl.status === 'loaded' || pl.status === 'degraded')).map(pl => pl.manifest?.id)
|
||||
);
|
||||
|
||||
workspaceTools = (contributions.workspaceItems || []).filter(tool => enabledIds.has(tool.pluginId));
|
||||
} catch (e) {
|
||||
console.error('[WorkspaceHost] loadTools error:', e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="workspace-host">
|
||||
{#if currentNode}
|
||||
<div class="workspace-header">
|
||||
<span class="workspace-title">{currentNode.title}</span>
|
||||
<span class="workspace-type">{currentNode.type}</span>
|
||||
</div>
|
||||
|
||||
{#if workspaceTools.length > 0}
|
||||
<div class="workspace-tools">
|
||||
{#each workspaceTools as tool (tool.id + tool.pluginId)}
|
||||
<div class="workspace-tool">
|
||||
<div class="tool-header">
|
||||
<span class="tool-title">{tool.title || tool.id}</span>
|
||||
<span class="tool-plugin">{tool.pluginId}</span>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<PluginBundleHost
|
||||
pluginId={tool.pluginId}
|
||||
componentId={tool.component}
|
||||
componentProps={{ workspaceNodeId: currentNodeId, workspaceNode: currentNode }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="workspace-empty">
|
||||
<p>No workspace tools available</p>
|
||||
<p class="workspace-hint">Install plugins that contribute workspaceItems to see tools here.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="workspace-empty">
|
||||
<p>Select a workspace node from the sidebar</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workspace-host {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.workspace-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #16213e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workspace-title {
|
||||
color: #e0e0f0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workspace-type {
|
||||
color: #4ecca3;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
background: #1a2a3a;
|
||||
}
|
||||
|
||||
.workspace-tools {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace-tool {
|
||||
border: 1px solid #16213e;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: #12122a;
|
||||
border-bottom: 1px solid #16213e;
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
color: #e0e0f0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-plugin {
|
||||
color: #666;
|
||||
font-size: 0.65rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tool-content {
|
||||
min-height: 300px;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.workspace-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workspace-hint {
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -78,6 +78,9 @@
|
|||
if (err) { localError = err; return; }
|
||||
currentNodeId = id;
|
||||
activeWorkspaceNodeId.set(id);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workspace-node-selected', {
|
||||
detail: { nodeId: id, nodes: nodes }
|
||||
}));
|
||||
}
|
||||
|
||||
function openCreate(parentId, type) {
|
||||
|
|
|
|||
|
|
@ -47,11 +47,10 @@
|
|||
{
|
||||
id: 'verstak.platform-test.markdown-diagnostic',
|
||||
title: 'Platform Test Markdown Diagnostic',
|
||||
priority: 100,
|
||||
priority: 10,
|
||||
component: 'MarkdownDiagnosticProvider',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -59,11 +58,86 @@
|
|||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||||
error: ''
|
||||
},
|
||||
'verstak.default-editor': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.default-editor',
|
||||
name: 'Default Editor',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Built-in text and markdown editor/viewer.',
|
||||
source: 'official',
|
||||
icon: 'edit',
|
||||
provides: ['verstak/default-editor/v1'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'files.write', 'workbench.open'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
openProviders: [
|
||||
{
|
||||
id: 'verstak.default-editor.text',
|
||||
title: 'Default Text Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log', '.conf', '.ini', '.toml', '.yaml', '.yml', '.json', '.csv'], mime: ['text/plain', 'application/json'], contexts: ['generic-text'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'verstak.default-editor.markdown',
|
||||
title: 'Default Markdown Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'verstak.default-editor.notes-markdown',
|
||||
title: 'Default Notes Markdown Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['notes-markdown'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/default-editor',
|
||||
error: ''
|
||||
},
|
||||
'verstak.files': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.files',
|
||||
name: 'Files',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Minimal vault file navigator.',
|
||||
source: 'official',
|
||||
icon: 'folder',
|
||||
provides: ['verstak/files/v1'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'files.write', 'workbench.open', 'ui.register'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
views: [{ id: 'verstak.files.view', title: 'Files', icon: 'folder', component: 'FilesView' }],
|
||||
workspaceItems: [{ id: 'verstak.files.workspace', title: 'Files', icon: 'folder', component: 'FilesView' }]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/files',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }] };
|
||||
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||
var workbenchPreferences = {};
|
||||
var openedResources = [];
|
||||
|
|
@ -97,7 +171,12 @@
|
|||
|
||||
function makeDefaultVaultFiles() {
|
||||
return {
|
||||
'': { type: 'folder', modifiedAt: new Date().toISOString() }
|
||||
'': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Docs': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Docs/todo.txt': { type: 'file', content: 'Buy groceries\nWrite tests', modifiedAt: new Date().toISOString() },
|
||||
'Docs/readme.md': { type: 'file', content: '# Hello World\n\nThis is a **test** document.\n\n- item 1\n- item 2', modifiedAt: new Date().toISOString() },
|
||||
'Notes': { type: 'folder', modifiedAt: new Date().toISOString() },
|
||||
'Notes/Overview.md': { type: 'file', content: '# Notes Overview\n\nMy notes content here.', modifiedAt: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +283,7 @@
|
|||
}
|
||||
|
||||
function allContributions() {
|
||||
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [];
|
||||
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [], workspaceItems = [];
|
||||
for (var id in pluginStates) {
|
||||
var s = pluginStates[id];
|
||||
var c = (s.manifest && s.manifest.contributes) || {};
|
||||
|
|
@ -214,8 +293,9 @@
|
|||
if (c.statusBarItems) c.statusBarItems.forEach(function (st) { statusBarItems.push(Object.assign({}, st, { pluginId: id })); });
|
||||
if (c.settingsPanels) c.settingsPanels.forEach(function (sp) { settingsPanels.push(Object.assign({}, sp, { pluginId: id })); });
|
||||
if (c.openProviders) c.openProviders.forEach(function (op) { openProviders.push(Object.assign({}, op, { pluginId: id })); });
|
||||
if (c.workspaceItems) c.workspaceItems.forEach(function (wi) { workspaceItems.push(Object.assign({}, wi, { pluginId: id })); });
|
||||
}
|
||||
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders };
|
||||
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders, workspaceItems: workspaceItems };
|
||||
}
|
||||
|
||||
function requestExtension(request) {
|
||||
|
|
@ -294,6 +374,118 @@
|
|||
return Promise.resolve([result, '']);
|
||||
}
|
||||
|
||||
function defaultEditorBundle() {
|
||||
return [
|
||||
'(function(){',
|
||||
'var DefaultEditor={',
|
||||
'mount:function(c,p,api){',
|
||||
'c.innerHTML="";',
|
||||
'c.className="de-root";',
|
||||
'var req=p.request||{};',
|
||||
'var path=req.path||"";',
|
||||
'var mode=req.mode||"view";',
|
||||
'var ctx=req.context||{};',
|
||||
'var isNotes=ctx.notesMode||ctx.isInsideNotesFolder;',
|
||||
'var ext=(req.extension||"").toLowerCase();',
|
||||
'var isMd=ext===".md"||ext===".markdown";',
|
||||
'var editorMode=isNotes?"notes-markdown":isMd?"generic-markdown":"text";',
|
||||
'c.setAttribute("data-editor-mode",editorMode);',
|
||||
'c.setAttribute("data-resource-path",path);',
|
||||
'c.setAttribute("data-request-mode",mode);',
|
||||
'var toolbar=document.createElement("div");',
|
||||
'toolbar.className="de-toolbar";',
|
||||
'var modeLabel=document.createElement("span");',
|
||||
'modeLabel.className="de-toolbar-mode";',
|
||||
'modeLabel.textContent=editorMode;',
|
||||
'toolbar.appendChild(modeLabel);',
|
||||
'var pathLabel=document.createElement("span");',
|
||||
'pathLabel.className="de-toolbar-context";',
|
||||
'pathLabel.textContent=path;',
|
||||
'toolbar.appendChild(pathLabel);',
|
||||
'if(isNotes){var badge=document.createElement("span");badge.className="de-notes-badge";badge.textContent="notes context";badge.setAttribute("data-notes-badge","");toolbar.appendChild(badge);}',
|
||||
'c.appendChild(toolbar);',
|
||||
'var content=document.createElement("div");',
|
||||
'content.className="de-editor-wrap";',
|
||||
'content.textContent="Loading...";',
|
||||
'c.appendChild(content);',
|
||||
'api.files.readText(path).then(function(text){',
|
||||
'content.textContent="";',
|
||||
'if(isMd){',
|
||||
'var preview=document.createElement("div");',
|
||||
'preview.className="de-preview";',
|
||||
'preview.setAttribute("data-preview","");',
|
||||
'preview.textContent=text;',
|
||||
'content.appendChild(preview);',
|
||||
'}else{',
|
||||
'var ta=document.createElement("textarea");',
|
||||
'ta.className="de-textarea";',
|
||||
'ta.value=text;',
|
||||
'ta.setAttribute("data-editor-textarea","");',
|
||||
'content.appendChild(ta);',
|
||||
'}',
|
||||
'}).catch(function(err){',
|
||||
'content.textContent="Error: "+(err.message||err);',
|
||||
'});',
|
||||
'},',
|
||||
'unmount:function(c){c.innerHTML="";}',
|
||||
'};',
|
||||
'window.VerstakPluginRegister("verstak.default-editor",{components:{DefaultEditor:DefaultEditor}});',
|
||||
'})();'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function filesPluginBundle() {
|
||||
return [
|
||||
"(function(){",
|
||||
"var FilesView={",
|
||||
"mount:function(c,p,api){",
|
||||
"c.innerHTML='';",
|
||||
"c.className='files-root';",
|
||||
"c.setAttribute('data-plugin-id','verstak.files');",
|
||||
"var list=document.createElement('div');",
|
||||
"list.className='files-list';",
|
||||
"list.setAttribute('data-files-list','');",
|
||||
"c.appendChild(list);",
|
||||
"function load(){",
|
||||
"list.textContent='Loading...';",
|
||||
"api.files.list('').then(function(entries){",
|
||||
"list.innerHTML='';",
|
||||
"if(!entries||!entries.length){list.textContent='Empty folder';return;}",
|
||||
"entries.forEach(function(e){",
|
||||
"if(e.isHidden||e.isReserved)return;",
|
||||
"var item=document.createElement('div');",
|
||||
"item.className='files-item';",
|
||||
"item.setAttribute('data-file-name',e.name);",
|
||||
"item.setAttribute('data-file-type',e.type);",
|
||||
"item.setAttribute('data-file-path',e.relativePath);",
|
||||
"var icon=document.createElement('span');",
|
||||
"icon.className='files-item-icon';",
|
||||
"icon.textContent=e.type==='folder'?'[D]':'[F]';",
|
||||
"var name=document.createElement('span');",
|
||||
"name.className='files-item-name';",
|
||||
"name.textContent=e.name;",
|
||||
"item.appendChild(icon);",
|
||||
"item.appendChild(name);",
|
||||
"if(e.type!=='folder'){",
|
||||
"item.addEventListener('dblclick',function(){",
|
||||
"var ext=e.extension?'.'+e.extension:'';",
|
||||
"var ctx={sourcePluginId:'verstak.files',sourceView:'files'};",
|
||||
"api.workbench.openResource({kind:'vault-file',path:e.relativePath,mode:'view',extension:ext,context:ctx});",
|
||||
"});",
|
||||
"}",
|
||||
"list.appendChild(item);",
|
||||
"});",
|
||||
"}).catch(function(err){list.textContent='Error: '+(err.message||err);});",
|
||||
"}",
|
||||
"load();",
|
||||
"},",
|
||||
"unmount:function(c){c.innerHTML='';}",
|
||||
"};",
|
||||
"window.VerstakPluginRegister('verstak.files',{components:{FilesView:FilesView}});",
|
||||
"})();"
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function platformTestBundle() {
|
||||
return [
|
||||
"(function(){",
|
||||
|
|
@ -448,6 +640,12 @@
|
|||
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(platformTestBundle());
|
||||
}
|
||||
if (pluginId === 'verstak.default-editor' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(defaultEditorBundle());
|
||||
}
|
||||
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
|
||||
return Promise.resolve(filesPluginBundle());
|
||||
}
|
||||
return Promise.resolve('');
|
||||
},
|
||||
GetPluginCapability: function (pluginId, capId) {
|
||||
|
|
@ -675,11 +873,10 @@
|
|||
{
|
||||
id: 'verstak.platform-test.markdown-diagnostic',
|
||||
title: 'Platform Test Markdown Diagnostic',
|
||||
priority: 100,
|
||||
priority: 10,
|
||||
component: 'MarkdownDiagnosticProvider',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -687,10 +884,85 @@
|
|||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/platform-test',
|
||||
error: ''
|
||||
},
|
||||
'verstak.default-editor': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.default-editor',
|
||||
name: 'Default Editor',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Built-in text and markdown editor/viewer.',
|
||||
source: 'official',
|
||||
icon: 'edit',
|
||||
provides: ['verstak/default-editor/v1'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'files.write', 'workbench.open'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
openProviders: [
|
||||
{
|
||||
id: 'verstak.default-editor.text',
|
||||
title: 'Default Text Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.txt', '.log', '.conf', '.ini', '.toml', '.yaml', '.yml', '.json', '.csv'], mime: ['text/plain', 'application/json'], contexts: ['generic-text'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'verstak.default-editor.markdown',
|
||||
title: 'Default Markdown Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'verstak.default-editor.notes-markdown',
|
||||
title: 'Default Notes Markdown Editor',
|
||||
priority: 50,
|
||||
component: 'DefaultEditor',
|
||||
supports: [
|
||||
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['notes-markdown'] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/default-editor',
|
||||
error: ''
|
||||
},
|
||||
'verstak.files': {
|
||||
status: 'loaded',
|
||||
enabled: true,
|
||||
manifest: {
|
||||
schemaVersion: 1,
|
||||
id: 'verstak.files',
|
||||
name: 'Files',
|
||||
version: '0.1.0',
|
||||
apiVersion: '0.1.0',
|
||||
description: 'Minimal vault file navigator.',
|
||||
source: 'official',
|
||||
icon: 'folder',
|
||||
provides: ['verstak/files/v1'],
|
||||
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
|
||||
permissions: ['files.read', 'files.write', 'workbench.open', 'ui.register'],
|
||||
frontend: { entry: 'frontend/dist/index.js' },
|
||||
contributes: {
|
||||
views: [{ id: 'verstak.files.view', title: 'Files', icon: 'folder', component: 'FilesView' }],
|
||||
workspaceItems: [{ id: 'verstak.files.workspace', title: 'Files', icon: 'folder', component: 'FilesView' }]
|
||||
}
|
||||
},
|
||||
rootPath: '/tmp/verstak-test/plugins/files',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
|
||||
vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
|
||||
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }] };
|
||||
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||
workbenchPreferences = {};
|
||||
openedResources = [];
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {workbench} from '../models';
|
||||
import {capability} from '../models';
|
||||
import {api} from '../models';
|
||||
import {permissions} from '../models';
|
||||
import {plugin} from '../models';
|
||||
import {files} from '../models';
|
||||
import {workbench} from '../models';
|
||||
|
||||
export function ArchiveWorkspaceNode(arg1:string):Promise<string>;
|
||||
|
||||
|
|
@ -19,12 +19,12 @@ export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise
|
|||
|
||||
export function DisablePlugin(arg1:string):Promise<string>;
|
||||
|
||||
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||
|
||||
export function EnablePlugin(arg1:string):Promise<string>;
|
||||
|
||||
export function ExecutePluginCommand(arg1:string,arg2:string,arg3:Record<string, any>):Promise<Record<string, any>|string>;
|
||||
|
||||
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
|
||||
|
||||
export function GetAppSettings():Promise<Record<string, any>>;
|
||||
|
||||
export function GetCapabilities():Promise<Array<capability.Entry>>;
|
||||
|
|
@ -49,12 +49,12 @@ export function GetVaultPluginState():Promise<Record<string, any>>;
|
|||
|
||||
export function GetVaultStatus():Promise<Record<string, string>>;
|
||||
|
||||
export function GetWorkspaceTree():Promise<Record<string, any>>;
|
||||
|
||||
export function GetWorkbenchOpenedResources():Promise<Array<workbench.OpenedResource>>;
|
||||
|
||||
export function GetWorkbenchPreferences():Promise<workbench.Preferences>;
|
||||
|
||||
export function GetWorkspaceTree():Promise<Record<string, any>>;
|
||||
|
||||
export function ListPluginCapabilities(arg1:string):Promise<Array<capability.Entry>|string>;
|
||||
|
||||
export function ListVaultFiles(arg1:string,arg2:string):Promise<Array<files.FileEntry>|string>;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export function DisablePlugin(arg1) {
|
|||
return window['go']['api']['App']['DisablePlugin'](arg1);
|
||||
}
|
||||
|
||||
export function EditWorkbenchResource(arg1, arg2) {
|
||||
return window['go']['api']['App']['EditWorkbenchResource'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function EnablePlugin(arg1) {
|
||||
return window['go']['api']['App']['EnablePlugin'](arg1);
|
||||
}
|
||||
|
|
@ -34,10 +38,6 @@ export function ExecutePluginCommand(arg1, arg2, arg3) {
|
|||
return window['go']['api']['App']['ExecutePluginCommand'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function EditWorkbenchResource(arg1, arg2) {
|
||||
return window['go']['api']['App']['EditWorkbenchResource'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetAppSettings() {
|
||||
return window['go']['api']['App']['GetAppSettings']();
|
||||
}
|
||||
|
|
@ -86,10 +86,6 @@ export function GetVaultStatus() {
|
|||
return window['go']['api']['App']['GetVaultStatus']();
|
||||
}
|
||||
|
||||
export function GetWorkspaceTree() {
|
||||
return window['go']['api']['App']['GetWorkspaceTree']();
|
||||
}
|
||||
|
||||
export function GetWorkbenchOpenedResources() {
|
||||
return window['go']['api']['App']['GetWorkbenchOpenedResources']();
|
||||
}
|
||||
|
|
@ -98,6 +94,10 @@ export function GetWorkbenchPreferences() {
|
|||
return window['go']['api']['App']['GetWorkbenchPreferences']();
|
||||
}
|
||||
|
||||
export function GetWorkspaceTree() {
|
||||
return window['go']['api']['App']['GetWorkspaceTree']();
|
||||
}
|
||||
|
||||
export function ListPluginCapabilities(arg1) {
|
||||
return window['go']['api']['App']['ListPluginCapabilities'](arg1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
export namespace api {
|
||||
|
||||
export class FlatWorkspaceItem {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
component: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new FlatWorkspaceItem(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.pluginId = source["pluginId"];
|
||||
this.id = source["id"];
|
||||
this.title = source["title"];
|
||||
this.icon = source["icon"];
|
||||
this.component = source["component"];
|
||||
}
|
||||
}
|
||||
export class FlatOpenProviderSupport {
|
||||
kind: string;
|
||||
mime?: string[];
|
||||
|
|
@ -146,6 +166,7 @@ export namespace api {
|
|||
settingsPanels: FlatSettingsPanel[];
|
||||
sidebarItems: FlatSidebarItem[];
|
||||
openProviders: FlatOpenProvider[];
|
||||
workspaceItems: FlatWorkspaceItem[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ContributionSummary(source);
|
||||
|
|
@ -158,6 +179,7 @@ export namespace api {
|
|||
this.settingsPanels = this.convertValues(source["settingsPanels"], FlatSettingsPanel);
|
||||
this.sidebarItems = this.convertValues(source["sidebarItems"], FlatSidebarItem);
|
||||
this.openProviders = this.convertValues(source["openProviders"], FlatOpenProvider);
|
||||
this.workspaceItems = this.convertValues(source["workspaceItems"], FlatWorkspaceItem);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
|
@ -182,166 +204,8 @@ export namespace api {
|
|||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export namespace workbench {
|
||||
|
||||
export class OpenResourceContext {
|
||||
sourcePluginId?: string;
|
||||
sourceView?: string;
|
||||
isInsideNotesFolder?: boolean;
|
||||
notesScopePath?: string;
|
||||
notesMode?: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceContext(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.sourcePluginId = source["sourcePluginId"];
|
||||
this.sourceView = source["sourceView"];
|
||||
this.isInsideNotesFolder = source["isInsideNotesFolder"];
|
||||
this.notesScopePath = source["notesScopePath"];
|
||||
this.notesMode = source["notesMode"];
|
||||
}
|
||||
}
|
||||
export class OpenResourceRequest {
|
||||
kind: string;
|
||||
path: string;
|
||||
mode?: string;
|
||||
mime?: string;
|
||||
extension?: string;
|
||||
context?: OpenResourceContext;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceRequest(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.kind = source["kind"];
|
||||
this.path = source["path"];
|
||||
this.mode = source["mode"];
|
||||
this.mime = source["mime"];
|
||||
this.extension = source["extension"];
|
||||
this.context = this.convertValues(source["context"], OpenResourceContext);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class OpenResourceResult {
|
||||
status: string;
|
||||
providerId?: string;
|
||||
providerPluginId?: string;
|
||||
providerComponent?: string;
|
||||
request: OpenResourceRequest;
|
||||
message?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.status = source["status"];
|
||||
this.providerId = source["providerId"];
|
||||
this.providerPluginId = source["providerPluginId"];
|
||||
this.providerComponent = source["providerComponent"];
|
||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||
this.message = source["message"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class OpenedResource {
|
||||
id: string;
|
||||
providerId: string;
|
||||
providerPluginId: string;
|
||||
providerComponent: string;
|
||||
request: OpenResourceRequest;
|
||||
openedAt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenedResource(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.providerId = source["providerId"];
|
||||
this.providerPluginId = source["providerPluginId"];
|
||||
this.providerComponent = source["providerComponent"];
|
||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||
this.openedAt = source["openedAt"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Preferences {
|
||||
defaultTextEditorProvider?: string;
|
||||
defaultMarkdownEditorProvider?: string;
|
||||
defaultNotesMarkdownEditorProvider?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Preferences(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.defaultTextEditorProvider = source["defaultTextEditorProvider"];
|
||||
this.defaultMarkdownEditorProvider = source["defaultMarkdownEditorProvider"];
|
||||
this.defaultNotesMarkdownEditorProvider = source["defaultNotesMarkdownEditorProvider"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -630,6 +494,62 @@ export namespace plugin {
|
|||
this.handler = source["handler"];
|
||||
}
|
||||
}
|
||||
export class OpenProviderSupport {
|
||||
kind: string;
|
||||
mime?: string[];
|
||||
extensions?: string[];
|
||||
contexts?: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenProviderSupport(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.kind = source["kind"];
|
||||
this.mime = source["mime"];
|
||||
this.extensions = source["extensions"];
|
||||
this.contexts = source["contexts"];
|
||||
}
|
||||
}
|
||||
export class ContributionOpenProvider {
|
||||
id: string;
|
||||
title: string;
|
||||
priority?: number;
|
||||
component: string;
|
||||
supports: OpenProviderSupport[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ContributionOpenProvider(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.title = source["title"];
|
||||
this.priority = source["priority"];
|
||||
this.component = source["component"];
|
||||
this.supports = this.convertValues(source["supports"], OpenProviderSupport);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class ContributionSearchProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -720,6 +640,24 @@ export namespace plugin {
|
|||
this.component = source["component"];
|
||||
}
|
||||
}
|
||||
export class ContributionWorkspaceItem {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
component: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ContributionWorkspaceItem(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.title = source["title"];
|
||||
this.icon = source["icon"];
|
||||
this.component = source["component"];
|
||||
}
|
||||
}
|
||||
export class Contributions {
|
||||
views?: ContributionView[];
|
||||
commands?: ContributionCommand[];
|
||||
|
|
@ -731,6 +669,8 @@ export namespace plugin {
|
|||
searchProviders?: ContributionSearchProvider[];
|
||||
activityProviders?: ContributionActivityProvider[];
|
||||
statusBarItems?: ContributionStatusBarItem[];
|
||||
openProviders?: ContributionOpenProvider[];
|
||||
workspaceItems?: ContributionWorkspaceItem[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Contributions(source);
|
||||
|
|
@ -748,6 +688,8 @@ export namespace plugin {
|
|||
this.searchProviders = this.convertValues(source["searchProviders"], ContributionSearchProvider);
|
||||
this.activityProviders = this.convertValues(source["activityProviders"], ContributionActivityProvider);
|
||||
this.statusBarItems = this.convertValues(source["statusBarItems"], ContributionStatusBarItem);
|
||||
this.openProviders = this.convertValues(source["openProviders"], ContributionOpenProvider);
|
||||
this.workspaceItems = this.convertValues(source["workspaceItems"], ContributionWorkspaceItem);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
|
|
@ -872,6 +814,7 @@ export namespace plugin {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export class Plugin {
|
||||
manifest: Manifest;
|
||||
status: string;
|
||||
|
|
@ -912,3 +855,165 @@ export namespace plugin {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace workbench {
|
||||
|
||||
export class OpenResourceContext {
|
||||
sourcePluginId?: string;
|
||||
sourceView?: string;
|
||||
isInsideNotesFolder?: boolean;
|
||||
notesScopePath?: string;
|
||||
notesMode?: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceContext(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.sourcePluginId = source["sourcePluginId"];
|
||||
this.sourceView = source["sourceView"];
|
||||
this.isInsideNotesFolder = source["isInsideNotesFolder"];
|
||||
this.notesScopePath = source["notesScopePath"];
|
||||
this.notesMode = source["notesMode"];
|
||||
}
|
||||
}
|
||||
export class OpenResourceRequest {
|
||||
kind: string;
|
||||
path: string;
|
||||
mode?: string;
|
||||
mime?: string;
|
||||
extension?: string;
|
||||
context?: OpenResourceContext;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceRequest(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.kind = source["kind"];
|
||||
this.path = source["path"];
|
||||
this.mode = source["mode"];
|
||||
this.mime = source["mime"];
|
||||
this.extension = source["extension"];
|
||||
this.context = this.convertValues(source["context"], OpenResourceContext);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class OpenResourceResult {
|
||||
status: string;
|
||||
providerId?: string;
|
||||
providerPluginId?: string;
|
||||
providerComponent?: string;
|
||||
request: OpenResourceRequest;
|
||||
message?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenResourceResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.status = source["status"];
|
||||
this.providerId = source["providerId"];
|
||||
this.providerPluginId = source["providerPluginId"];
|
||||
this.providerComponent = source["providerComponent"];
|
||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||
this.message = source["message"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class OpenedResource {
|
||||
id: string;
|
||||
providerId: string;
|
||||
providerPluginId: string;
|
||||
providerComponent: string;
|
||||
request: OpenResourceRequest;
|
||||
openedAt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new OpenedResource(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.providerId = source["providerId"];
|
||||
this.providerPluginId = source["providerPluginId"];
|
||||
this.providerComponent = source["providerComponent"];
|
||||
this.request = this.convertValues(source["request"], OpenResourceRequest);
|
||||
this.openedAt = source["openedAt"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Preferences {
|
||||
defaultTextEditorProvider?: string;
|
||||
defaultMarkdownEditorProvider?: string;
|
||||
defaultNotesMarkdownEditorProvider?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Preferences(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.defaultTextEditorProvider = source["defaultTextEditorProvider"];
|
||||
this.defaultMarkdownEditorProvider = source["defaultMarkdownEditorProvider"];
|
||||
this.defaultNotesMarkdownEditorProvider = source["defaultNotesMarkdownEditorProvider"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,14 @@ type FlatOpenProvider struct {
|
|||
Supports []FlatOpenProviderSupport `json:"supports"`
|
||||
}
|
||||
|
||||
type FlatWorkspaceItem struct {
|
||||
PluginID string `json:"pluginId"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Component string `json:"component"`
|
||||
}
|
||||
|
||||
// ContributionSummary aggregates all contribution types for the frontend.
|
||||
type ContributionSummary struct {
|
||||
Views []FlatView `json:"views"`
|
||||
|
|
@ -247,6 +255,7 @@ type ContributionSummary struct {
|
|||
SettingsPanels []FlatSettingsPanel `json:"settingsPanels"`
|
||||
SidebarItems []FlatSidebarItem `json:"sidebarItems"`
|
||||
OpenProviders []FlatOpenProvider `json:"openProviders"`
|
||||
WorkspaceItems []FlatWorkspaceItem `json:"workspaceItems"`
|
||||
}
|
||||
|
||||
// buildContributionSummary creates a ContributionSummary from the registry.
|
||||
|
|
@ -259,6 +268,7 @@ func buildContributionSummary(r *contribution.Registry) ContributionSummary {
|
|||
regPanels := r.SettingsPanels()
|
||||
regSidebar := r.SidebarItems()
|
||||
regOpenProviders := r.OpenProviders()
|
||||
regWorkspaceItems := r.WorkspaceItems()
|
||||
|
||||
views := make([]FlatView, len(regViews))
|
||||
for i, v := range regViews {
|
||||
|
|
@ -291,7 +301,11 @@ func buildContributionSummary(r *contribution.Registry) ContributionSummary {
|
|||
Supports: supports,
|
||||
}
|
||||
}
|
||||
return ContributionSummary{Views: views, Commands: cmds, SettingsPanels: panels, SidebarItems: sidebar, OpenProviders: openProviders}
|
||||
workspaceItems := make([]FlatWorkspaceItem, len(regWorkspaceItems))
|
||||
for i, v := range regWorkspaceItems {
|
||||
workspaceItems[i] = FlatWorkspaceItem{PluginID: v.PluginID, ID: v.Item.ID, Title: v.Item.Title, Icon: v.Item.Icon, Component: v.Item.Component}
|
||||
}
|
||||
return ContributionSummary{Views: views, Commands: cmds, SettingsPanels: panels, SidebarItems: sidebar, OpenProviders: openProviders, WorkspaceItems: workspaceItems}
|
||||
}
|
||||
|
||||
// GetContributions returns all registered contributions flattened for the frontend.
|
||||
|
|
|
|||
|
|
@ -668,6 +668,25 @@ func TestWorkbenchOpenResourceRequiresPermission(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWorkbenchDisabledPluginProviderExcluded(t *testing.T) {
|
||||
app := newBridgeTestApp(t)
|
||||
|
||||
result, errStr := app.OpenWorkbenchResource("bridge.plugin", map[string]interface{}{
|
||||
"kind": "vault-file",
|
||||
"path": "Docs/readme.md",
|
||||
"extension": ".md",
|
||||
})
|
||||
if errStr != "" {
|
||||
t.Fatalf("OpenWorkbenchResource: %s", errStr)
|
||||
}
|
||||
if result.ProviderPluginID != "bridge.plugin" {
|
||||
t.Fatalf("expected bridge.plugin provider, got providerPluginId=%q", result.ProviderPluginID)
|
||||
}
|
||||
if result.ProviderID == "disabled.markdown" {
|
||||
t.Fatal("disabled plugin provider should be excluded from selection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginBridgeSettingsRequireLoadedPluginAndStoragePermission(t *testing.T) {
|
||||
app := newBridgeTestApp(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type Registry struct {
|
|||
activityProviders []ContributionActivityProvider
|
||||
statusBarItems []ContributionStatusBarItem
|
||||
openProviders []ContributionOpenProvider
|
||||
workspaceItems []ContributionWorkspaceItem
|
||||
}
|
||||
|
||||
// ContributionPointType defines the type of contribution point.
|
||||
|
|
@ -40,6 +41,7 @@ const (
|
|||
PointActivity ContributionPointType = "activityProviders"
|
||||
PointStatusBar ContributionPointType = "statusBarItems"
|
||||
PointOpenProviders ContributionPointType = "openProviders"
|
||||
PointWorkspaceItems ContributionPointType = "workspaceItems"
|
||||
)
|
||||
|
||||
// ListByPoint returns all contributions for a given point type.
|
||||
|
|
@ -93,6 +95,10 @@ func (r *Registry) ListByPoint(point ContributionPointType) []interface{} {
|
|||
for _, v := range r.openProviders {
|
||||
result = append(result, v)
|
||||
}
|
||||
case PointWorkspaceItems:
|
||||
for _, v := range r.workspaceItems {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -147,6 +153,12 @@ type ContributionOpenProvider struct {
|
|||
Item plugin.ContributionOpenProvider `json:"item"`
|
||||
}
|
||||
|
||||
// ContributionWorkspaceItem is a workspace tool contribution.
|
||||
type ContributionWorkspaceItem struct {
|
||||
PluginID string `json:"pluginId"`
|
||||
Item plugin.ContributionWorkspaceItem `json:"item"`
|
||||
}
|
||||
|
||||
// NewRegistry creates a new contribution registry.
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{}
|
||||
|
|
@ -171,6 +183,7 @@ func (r *Registry) Register(pluginID string, c *plugin.Contributions) {
|
|||
r.activityProviders = removeActivityProviders(r.activityProviders, pluginID)
|
||||
r.statusBarItems = removeStatusBarItems(r.statusBarItems, pluginID)
|
||||
r.openProviders = removeOpenProviders(r.openProviders, pluginID)
|
||||
r.workspaceItems = removeWorkspaceItems(r.workspaceItems, pluginID)
|
||||
|
||||
for _, item := range c.Views {
|
||||
r.views = append(r.views, ContributionView{PluginID: pluginID, Item: item})
|
||||
|
|
@ -205,6 +218,9 @@ func (r *Registry) Register(pluginID string, c *plugin.Contributions) {
|
|||
for _, item := range c.OpenProviders {
|
||||
r.openProviders = append(r.openProviders, ContributionOpenProvider{PluginID: pluginID, Item: item})
|
||||
}
|
||||
for _, item := range c.WorkspaceItems {
|
||||
r.workspaceItems = append(r.workspaceItems, ContributionWorkspaceItem{PluginID: pluginID, Item: item})
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister removes all contributions from a plugin.
|
||||
|
|
@ -223,6 +239,7 @@ func (r *Registry) Unregister(pluginID string) {
|
|||
r.activityProviders = removeActivityProviders(r.activityProviders, pluginID)
|
||||
r.statusBarItems = removeStatusBarItems(r.statusBarItems, pluginID)
|
||||
r.openProviders = removeOpenProviders(r.openProviders, pluginID)
|
||||
r.workspaceItems = removeWorkspaceItems(r.workspaceItems, pluginID)
|
||||
}
|
||||
|
||||
// Getters — sorted for deterministic display.
|
||||
|
|
@ -304,6 +321,20 @@ func (r *Registry) OpenProviders() []ContributionOpenProvider {
|
|||
return result
|
||||
}
|
||||
|
||||
func (r *Registry) WorkspaceItems() []ContributionWorkspaceItem {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]ContributionWorkspaceItem, len(r.workspaceItems))
|
||||
copy(result, r.workspaceItems)
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].PluginID != result[j].PluginID {
|
||||
return result[i].PluginID < result[j].PluginID
|
||||
}
|
||||
return result[i].Item.ID < result[j].Item.ID
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// ─── Remove helpers ─────────────────────────────────────────
|
||||
|
||||
func removeViews(items []ContributionView, pluginID string) []ContributionView {
|
||||
|
|
@ -405,3 +436,13 @@ func removeOpenProviders(items []ContributionOpenProvider, pluginID string) []Co
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func removeWorkspaceItems(items []ContributionWorkspaceItem, pluginID string) []ContributionWorkspaceItem {
|
||||
var result []ContributionWorkspaceItem
|
||||
for _, item := range items {
|
||||
if item.PluginID != pluginID {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ type Contributions struct {
|
|||
ActivityProviders []ContributionActivityProvider `json:"activityProviders,omitempty"`
|
||||
StatusBarItems []ContributionStatusBarItem `json:"statusBarItems,omitempty"`
|
||||
OpenProviders []ContributionOpenProvider `json:"openProviders,omitempty"`
|
||||
WorkspaceItems []ContributionWorkspaceItem `json:"workspaceItems,omitempty"`
|
||||
}
|
||||
|
||||
// ContributionView represents a view contribution.
|
||||
|
|
@ -162,6 +163,14 @@ type ContributionOpenProvider struct {
|
|||
Supports []OpenProviderSupport `json:"supports"`
|
||||
}
|
||||
|
||||
// ContributionWorkspaceItem represents a workspace tool contribution.
|
||||
type ContributionWorkspaceItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Component string `json:"component"`
|
||||
}
|
||||
|
||||
// SyncConfig describes plugin sync configuration.
|
||||
type SyncConfig struct {
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
|
|
|
|||
|
|
@ -205,6 +205,187 @@ func TestOpenResourceReturnsNoProviderFallback(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderUsesTextPreference(t *testing.T) {
|
||||
r := NewRouter(Preferences{
|
||||
DefaultTextEditorProvider: "custom.text-editor",
|
||||
})
|
||||
providers := []contribution.ContributionOpenProvider{
|
||||
provider("official.editor", "builtin.text", 100, "BuiltinText", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".txt"},
|
||||
Contexts: []string{ContextGenericText},
|
||||
}),
|
||||
provider("custom.editor", "custom.text-editor", 10, "CustomText", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".txt"},
|
||||
Contexts: []string{ContextGenericText},
|
||||
}),
|
||||
}
|
||||
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: "Docs/todo.txt",
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "custom.text-editor" {
|
||||
t.Fatalf("provider = %q, want custom.text-editor", selected.Item.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderUsesMarkdownPreference(t *testing.T) {
|
||||
r := NewRouter(Preferences{
|
||||
DefaultMarkdownEditorProvider: "community.markdown-editor",
|
||||
})
|
||||
providers := []contribution.ContributionOpenProvider{
|
||||
provider("official.editor", "builtin.markdown", 100, "BuiltinMarkdown", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".md"},
|
||||
Contexts: []string{ContextGenericMarkdown},
|
||||
}),
|
||||
provider("community.editor", "community.markdown-editor", 10, "CommunityMarkdown", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".md"},
|
||||
Contexts: []string{ContextGenericMarkdown},
|
||||
}),
|
||||
}
|
||||
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: "Docs/readme.md",
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "community.markdown-editor" {
|
||||
t.Fatalf("provider = %q, want community.markdown-editor", selected.Item.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderMatchesMime(t *testing.T) {
|
||||
r := NewRouter(Preferences{})
|
||||
providers := []contribution.ContributionOpenProvider{
|
||||
provider("image.plugin", "image.viewer", 10, "ImageViewer", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Mime: []string{"image/png", "image/jpeg"},
|
||||
}),
|
||||
}
|
||||
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: "Photos/screenshot.png",
|
||||
Extension: ".png",
|
||||
Mime: "image/png",
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "image.viewer" {
|
||||
t.Fatalf("provider = %q, want image.viewer", selected.Item.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderExtensionCaseInsensitive(t *testing.T) {
|
||||
r := NewRouter(Preferences{})
|
||||
providers := []contribution.ContributionOpenProvider{
|
||||
provider("editor.plugin", "md.editor", 10, "MDEditor", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".md"},
|
||||
}),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ext string
|
||||
path string
|
||||
}{
|
||||
{"uppercase .MD", ".MD", "Docs/README.MD"},
|
||||
{"mixed case .Md", ".Md", "Docs/Notes.Md"},
|
||||
{"lowercase .md", ".md", "Docs/readme.md"},
|
||||
{"markdown extension uppercase", ".MD", "Docs/guide.MD"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: tt.path,
|
||||
Extension: tt.ext,
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "md.editor" {
|
||||
t.Fatalf("provider = %q, want md.editor for ext %s", selected.Item.ID, tt.ext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderMultipleSupportsEntries(t *testing.T) {
|
||||
r := NewRouter(Preferences{})
|
||||
providers := []contribution.ContributionOpenProvider{
|
||||
provider("editor.plugin", "multi.editor", 10, "MultiEditor", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".md"},
|
||||
Contexts: []string{ContextGenericMarkdown},
|
||||
}, plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".txt"},
|
||||
Contexts: []string{ContextGenericText},
|
||||
}),
|
||||
}
|
||||
|
||||
t.Run("matches markdown entry", func(t *testing.T) {
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: "Docs/readme.md",
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "multi.editor" {
|
||||
t.Fatalf("provider = %q, want multi.editor", selected.Item.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("matches text entry", func(t *testing.T) {
|
||||
selected, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "vault-file",
|
||||
Path: "Docs/todo.txt",
|
||||
Mode: "view",
|
||||
}, providers)
|
||||
if err != nil {
|
||||
t.Fatalf("SelectProvider: %v", err)
|
||||
}
|
||||
if selected.Item.ID != "multi.editor" {
|
||||
t.Fatalf("provider = %q, want multi.editor", selected.Item.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSelectProviderKindMismatch(t *testing.T) {
|
||||
r := NewRouter(Preferences{})
|
||||
_, err := r.SelectProvider(OpenResourceRequest{
|
||||
Kind: "http-url",
|
||||
Path: "https://example.com/file.md",
|
||||
Mode: "view",
|
||||
}, []contribution.ContributionOpenProvider{
|
||||
provider("editor.plugin", "vault.editor", 10, "VaultEditor", plugin.OpenProviderSupport{
|
||||
Kind: "vault-file",
|
||||
Extensions: []string{".md"},
|
||||
}),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected no provider for http-url kind with vault-file provider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineContextName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Reference in New Issue