Implement milestone 6b workbench routing skeleton

This commit is contained in:
2026-06-19 07:51:57 +08:00
parent a100f5a441
commit 6ed6df311a
53 changed files with 7592 additions and 335 deletions
+40
View File
@@ -22,6 +22,7 @@ type Registry struct {
searchProviders []ContributionSearchProvider
activityProviders []ContributionActivityProvider
statusBarItems []ContributionStatusBarItem
openProviders []ContributionOpenProvider
}
// ContributionPointType defines the type of contribution point.
@@ -38,6 +39,7 @@ const (
PointSearchProviders ContributionPointType = "searchProviders"
PointActivity ContributionPointType = "activityProviders"
PointStatusBar ContributionPointType = "statusBarItems"
PointOpenProviders ContributionPointType = "openProviders"
)
// ListByPoint returns all contributions for a given point type.
@@ -87,6 +89,10 @@ func (r *Registry) ListByPoint(point ContributionPointType) []interface{} {
for _, v := range r.statusBarItems {
result = append(result, v)
}
case PointOpenProviders:
for _, v := range r.openProviders {
result = append(result, v)
}
}
return result
}
@@ -136,6 +142,11 @@ type ContributionStatusBarItem struct {
Item plugin.ContributionStatusBarItem `json:"item"`
}
type ContributionOpenProvider struct {
PluginID string `json:"pluginId"`
Item plugin.ContributionOpenProvider `json:"item"`
}
// NewRegistry creates a new contribution registry.
func NewRegistry() *Registry {
return &Registry{}
@@ -159,6 +170,7 @@ func (r *Registry) Register(pluginID string, c *plugin.Contributions) {
r.searchProviders = removeSearchProviders(r.searchProviders, pluginID)
r.activityProviders = removeActivityProviders(r.activityProviders, pluginID)
r.statusBarItems = removeStatusBarItems(r.statusBarItems, pluginID)
r.openProviders = removeOpenProviders(r.openProviders, pluginID)
for _, item := range c.Views {
r.views = append(r.views, ContributionView{PluginID: pluginID, Item: item})
@@ -190,6 +202,9 @@ func (r *Registry) Register(pluginID string, c *plugin.Contributions) {
for _, item := range c.StatusBarItems {
r.statusBarItems = append(r.statusBarItems, ContributionStatusBarItem{PluginID: pluginID, Item: item})
}
for _, item := range c.OpenProviders {
r.openProviders = append(r.openProviders, ContributionOpenProvider{PluginID: pluginID, Item: item})
}
}
// Unregister removes all contributions from a plugin.
@@ -207,6 +222,7 @@ func (r *Registry) Unregister(pluginID string) {
r.searchProviders = removeSearchProviders(r.searchProviders, pluginID)
r.activityProviders = removeActivityProviders(r.activityProviders, pluginID)
r.statusBarItems = removeStatusBarItems(r.statusBarItems, pluginID)
r.openProviders = removeOpenProviders(r.openProviders, pluginID)
}
// Getters — sorted for deterministic display.
@@ -274,6 +290,20 @@ func (r *Registry) SearchProviders() []ContributionSearchProvider {
return result
}
func (r *Registry) OpenProviders() []ContributionOpenProvider {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]ContributionOpenProvider, len(r.openProviders))
copy(result, r.openProviders)
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 {
@@ -365,3 +395,13 @@ func removeStatusBarItems(items []ContributionStatusBarItem, pluginID string) []
}
return result
}
func removeOpenProviders(items []ContributionOpenProvider, pluginID string) []ContributionOpenProvider {
var result []ContributionOpenProvider
for _, item := range items {
if item.PluginID != pluginID {
result = append(result, item)
}
}
return result
}
@@ -184,6 +184,17 @@ func TestListByPoint(t *testing.T) {
SearchProviders: []plugin.ContributionSearchProvider{{ID: "sp1", Label: "SP1", Handler: "h"}},
ActivityProviders: []plugin.ContributionActivityProvider{{ID: "ap1", Events: []string{"test"}, Handler: "h"}},
StatusBarItems: []plugin.ContributionStatusBarItem{{ID: "sb1", Label: "SB1"}},
OpenProviders: []plugin.ContributionOpenProvider{{
ID: "op1",
Title: "Open Provider 1",
Priority: 100,
Component: "OpenProvider",
Supports: []plugin.OpenProviderSupport{{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{"generic-markdown", "notes-markdown"},
}},
}},
}
r.Register("test.plugin", contrib)
@@ -202,6 +213,7 @@ func TestListByPoint(t *testing.T) {
{PointSearchProviders, 1},
{PointActivity, 1},
{PointStatusBar, 1},
{PointOpenProviders, 1},
}
for _, tt := range tests {
@@ -212,6 +224,55 @@ func TestListByPoint(t *testing.T) {
}
}
func TestOpenProviders_RegisterReplaceUnregister(t *testing.T) {
r := NewRegistry()
r.Register("editor.plugin", &plugin.Contributions{
OpenProviders: []plugin.ContributionOpenProvider{{
ID: "editor.markdown",
Title: "Markdown",
Priority: 50,
Component: "MarkdownEditor",
Supports: []plugin.OpenProviderSupport{{
Kind: "vault-file",
Extensions: []string{".md", ".markdown"},
Contexts: []string{"generic-markdown", "notes-markdown"},
}},
}},
})
providers := r.OpenProviders()
if len(providers) != 1 {
t.Fatalf("OpenProviders count = %d, want 1", len(providers))
}
if providers[0].PluginID != "editor.plugin" || providers[0].Item.Component != "MarkdownEditor" {
t.Fatalf("provider = %+v", providers[0])
}
r.Register("editor.plugin", &plugin.Contributions{
OpenProviders: []plugin.ContributionOpenProvider{{
ID: "editor.text",
Title: "Text",
Priority: 10,
Component: "TextEditor",
Supports: []plugin.OpenProviderSupport{{
Kind: "vault-file",
Extensions: []string{".txt"},
}},
}},
})
providers = r.OpenProviders()
if len(providers) != 1 || providers[0].Item.ID != "editor.text" {
t.Fatalf("providers after replace = %+v", providers)
}
r.Unregister("editor.plugin")
if got := len(r.OpenProviders()); got != 0 {
t.Fatalf("OpenProviders after unregister = %d, want 0", got)
}
}
// TestRegister_DuplicatePrevention calls Register twice for the same plugin
// (simulating reload) and checks contributions appear only once (no duplicates).
// This is the KEY TEST for idempotent re-registration.