Route open providers by request mode

This commit is contained in:
mirivlad 2026-06-27 13:49:09 +08:00
parent f87f8235bd
commit fb68c54409
8 changed files with 71 additions and 6 deletions

View File

@ -97,8 +97,8 @@ Frontend bundles are mounted with a plugin-scoped API created by
a concrete editor plugin. a concrete editor plugin.
Editor/viewer plugins contribute providers with `contributes.openProviders`. Editor/viewer plugins contribute providers with `contributes.openProviders`.
Workbench selects by resource kind, extension/mime, context (`generic-text`, Workbench selects by resource kind, request mode, extension/mime, context
`generic-markdown`, `notes-markdown`), user preference, priority, then (`generic-text`, `generic-markdown`, `notes-markdown`), user preference, priority, then
deterministic `pluginId/providerId` tie-break. If nothing matches, Workbench deterministic `pluginId/providerId` tie-break. If nothing matches, Workbench
shows `no-provider` fallback instead of a core editor. shows `no-provider` fallback instead of a core editor.

View File

@ -317,7 +317,8 @@ the Workbench helper.
{ {
"kind": "vault-file", "kind": "vault-file",
"extensions": [".md", ".markdown"], "extensions": [".md", ".markdown"],
"contexts": ["generic-markdown", "notes-markdown"] "contexts": ["generic-markdown", "notes-markdown"],
"modes": ["view"]
}, },
{ {
"kind": "vault-file", "kind": "vault-file",
@ -333,7 +334,7 @@ the Workbench helper.
``` ```
Selection uses enabled loaded/degraded provider plugins, resource kind, Selection uses enabled loaded/degraded provider plugins, resource kind,
extension/mime, context, user preference, priority, then deterministic request mode, extension/mime, context, user preference, priority, then deterministic
`pluginId/providerId` fallback. If nothing matches, Workbench returns `pluginId/providerId` fallback. If nothing matches, Workbench returns
`status: "no-provider"` and shows the fallback view instead of a core editor. `status: "no-provider"` and shows the fallback view instead of a core editor.

View File

@ -339,8 +339,10 @@
function providerSupports(provider, request) { function providerSupports(provider, request) {
var ext = requestExtension(request); var ext = requestExtension(request);
var contextName = requestContextName(request); var contextName = requestContextName(request);
var mode = String((request && request.mode) || 'view').toLowerCase();
return (provider.supports || []).some(function (support) { return (provider.supports || []).some(function (support) {
if (support.kind && support.kind !== request.kind) return false; if (support.kind && support.kind !== request.kind) return false;
if (support.modes && support.modes.length && support.modes.map(function (m) { return String(m).toLowerCase(); }).indexOf(mode) === -1) return false;
if (support.extensions && support.extensions.length && support.extensions.map(function (e) { return String(e).toLowerCase(); }).indexOf(ext) === -1) return false; if (support.extensions && support.extensions.length && support.extensions.map(function (e) { return String(e).toLowerCase(); }).indexOf(ext) === -1) return false;
if (support.contexts && support.contexts.length && support.contexts.indexOf(contextName) === -1) return false; if (support.contexts && support.contexts.length && support.contexts.indexOf(contextName) === -1) return false;
return true; return true;

View File

@ -25,6 +25,7 @@ export namespace api {
mime?: string[]; mime?: string[];
extensions?: string[]; extensions?: string[];
contexts?: string[]; contexts?: string[];
modes?: string[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new FlatOpenProviderSupport(source); return new FlatOpenProviderSupport(source);
@ -36,6 +37,7 @@ export namespace api {
this.mime = source["mime"]; this.mime = source["mime"];
this.extensions = source["extensions"]; this.extensions = source["extensions"];
this.contexts = source["contexts"]; this.contexts = source["contexts"];
this.modes = source["modes"];
} }
} }
export class FlatOpenProvider { export class FlatOpenProvider {
@ -534,6 +536,7 @@ export namespace plugin {
mime?: string[]; mime?: string[];
extensions?: string[]; extensions?: string[];
contexts?: string[]; contexts?: string[];
modes?: string[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new OpenProviderSupport(source); return new OpenProviderSupport(source);
@ -545,6 +548,7 @@ export namespace plugin {
this.mime = source["mime"]; this.mime = source["mime"];
this.extensions = source["extensions"]; this.extensions = source["extensions"];
this.contexts = source["contexts"]; this.contexts = source["contexts"];
this.modes = source["modes"];
} }
} }
export class ContributionOpenProvider { export class ContributionOpenProvider {
@ -1159,4 +1163,3 @@ export namespace workspace {
} }
} }

View File

@ -249,6 +249,7 @@ type FlatOpenProviderSupport struct {
Mime []string `json:"mime,omitempty"` Mime []string `json:"mime,omitempty"`
Extensions []string `json:"extensions,omitempty"` Extensions []string `json:"extensions,omitempty"`
Contexts []string `json:"contexts,omitempty"` Contexts []string `json:"contexts,omitempty"`
Modes []string `json:"modes,omitempty"`
} }
type FlatOpenProvider struct { type FlatOpenProvider struct {
@ -316,7 +317,7 @@ func buildContributionSummary(r *contribution.Registry) ContributionSummary {
for i, v := range regOpenProviders { for i, v := range regOpenProviders {
supports := make([]FlatOpenProviderSupport, len(v.Item.Supports)) supports := make([]FlatOpenProviderSupport, len(v.Item.Supports))
for j, s := range v.Item.Supports { for j, s := range v.Item.Supports {
supports[j] = FlatOpenProviderSupport{Kind: s.Kind, Mime: s.Mime, Extensions: s.Extensions, Contexts: s.Contexts} supports[j] = FlatOpenProviderSupport{Kind: s.Kind, Mime: s.Mime, Extensions: s.Extensions, Contexts: s.Contexts, Modes: s.Modes}
} }
openProviders[i] = FlatOpenProvider{ openProviders[i] = FlatOpenProvider{
PluginID: v.PluginID, PluginID: v.PluginID,

View File

@ -152,6 +152,7 @@ type OpenProviderSupport struct {
Mime []string `json:"mime,omitempty"` Mime []string `json:"mime,omitempty"`
Extensions []string `json:"extensions,omitempty"` Extensions []string `json:"extensions,omitempty"`
Contexts []string `json:"contexts,omitempty"` Contexts []string `json:"contexts,omitempty"`
Modes []string `json:"modes,omitempty"`
} }
// ContributionOpenProvider represents an editor/viewer provider contribution. // ContributionOpenProvider represents an editor/viewer provider contribution.

View File

@ -168,6 +168,9 @@ func providerMatches(request OpenResourceRequest, provider plugin.ContributionOp
if support.Kind != request.Kind { if support.Kind != request.Kind {
continue continue
} }
if !supportMatchesMode(request, support) {
continue
}
if !supportMatchesExtensionOrMime(request, support) { if !supportMatchesExtensionOrMime(request, support) {
continue continue
} }
@ -179,6 +182,19 @@ func providerMatches(request OpenResourceRequest, provider plugin.ContributionOp
return false return false
} }
func supportMatchesMode(request OpenResourceRequest, support plugin.OpenProviderSupport) bool {
if len(support.Modes) == 0 {
return true
}
mode := strings.ToLower(request.Mode)
for _, supported := range support.Modes {
if strings.ToLower(supported) == mode {
return true
}
}
return false
}
func supportMatchesExtensionOrMime(request OpenResourceRequest, support plugin.OpenProviderSupport) bool { func supportMatchesExtensionOrMime(request OpenResourceRequest, support plugin.OpenProviderSupport) bool {
hasExtensionRules := len(support.Extensions) > 0 hasExtensionRules := len(support.Extensions) > 0
hasMimeRules := len(support.Mime) > 0 hasMimeRules := len(support.Mime) > 0

View File

@ -91,6 +91,47 @@ func TestSelectProviderFallsBackByPriorityThenID(t *testing.T) {
} }
} }
func TestSelectProviderHonorsSupportModes(t *testing.T) {
r := NewRouter(Preferences{})
providers := []contribution.ContributionOpenProvider{
provider("preview.plugin", "markdown.preview", 100, "MarkdownPreview", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{ContextGenericMarkdown},
Modes: []string{"view"},
}),
provider("editor.plugin", "markdown.editor", 50, "MarkdownEditor", plugin.OpenProviderSupport{
Kind: "vault-file",
Extensions: []string{".md"},
Contexts: []string{ContextGenericMarkdown},
}),
}
viewProvider, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/readme.md",
Mode: "view",
}, providers)
if err != nil {
t.Fatalf("SelectProvider(view): %v", err)
}
if viewProvider.Item.ID != "markdown.preview" {
t.Fatalf("view provider = %q, want markdown.preview", viewProvider.Item.ID)
}
editProvider, err := r.SelectProvider(OpenResourceRequest{
Kind: "vault-file",
Path: "Docs/readme.md",
Mode: "edit",
}, providers)
if err != nil {
t.Fatalf("SelectProvider(edit): %v", err)
}
if editProvider.Item.ID != "markdown.editor" {
t.Fatalf("edit provider = %q, want markdown.editor", editProvider.Item.ID)
}
}
func TestSelectProviderTieBreaksByPluginIDThenProviderID(t *testing.T) { func TestSelectProviderTieBreaksByPluginIDThenProviderID(t *testing.T) {
r := NewRouter(Preferences{}) r := NewRouter(Preferences{})
providers := []contribution.ContributionOpenProvider{ providers := []contribution.ContributionOpenProvider{