verstak-desktop/internal/core/workbench/router.go

258 lines
7.5 KiB
Go

// Package workbench routes open/edit resource requests to contributed providers.
package workbench
import (
"fmt"
"path"
"sort"
"strings"
"time"
"github.com/verstak/verstak-desktop/internal/core/contribution"
"github.com/verstak/verstak-desktop/internal/core/plugin"
)
type Preferences struct {
DefaultTextEditorProvider string `json:"defaultTextEditorProvider,omitempty"`
DefaultMarkdownEditorProvider string `json:"defaultMarkdownEditorProvider,omitempty"`
DefaultNotesMarkdownEditorProvider string `json:"defaultNotesMarkdownEditorProvider,omitempty"`
}
const (
ContextGenericText = "generic-text"
ContextGenericMarkdown = "generic-markdown"
ContextNotesMarkdown = "notes-markdown"
)
type OpenResourceContext struct {
SourcePluginID string `json:"sourcePluginId,omitempty"`
SourceView string `json:"sourceView,omitempty"`
IsInsideNotesFolder bool `json:"isInsideNotesFolder,omitempty"`
NotesScopePath string `json:"notesScopePath,omitempty"`
NotesMode bool `json:"notesMode,omitempty"`
}
type OpenResourceRequest struct {
Kind string `json:"kind"`
Path string `json:"path"`
Mode string `json:"mode,omitempty"`
Mime string `json:"mime,omitempty"`
Extension string `json:"extension,omitempty"`
Context OpenResourceContext `json:"context,omitempty"`
}
type OpenResourceResult struct {
Status string `json:"status"`
ProviderID string `json:"providerId,omitempty"`
ProviderPluginID string `json:"providerPluginId,omitempty"`
ProviderComponent string `json:"providerComponent,omitempty"`
Request OpenResourceRequest `json:"request"`
Message string `json:"message,omitempty"`
}
type OpenedResource struct {
ID string `json:"id"`
ProviderID string `json:"providerId"`
ProviderPluginID string `json:"providerPluginId"`
ProviderComponent string `json:"providerComponent"`
Request OpenResourceRequest `json:"request"`
OpenedAt string `json:"openedAt"`
}
type Router struct {
preferences Preferences
opened []OpenedResource
}
func NewRouter(preferences Preferences) *Router {
return &Router{preferences: preferences}
}
func (r *Router) Preferences() Preferences {
return r.preferences
}
func (r *Router) SetPreferences(preferences Preferences) {
r.preferences = preferences
}
func (r *Router) SelectProvider(request OpenResourceRequest, providers []contribution.ContributionOpenProvider) (contribution.ContributionOpenProvider, error) {
request = normalizeRequest(request)
var matches []contribution.ContributionOpenProvider
for _, provider := range providers {
if providerMatches(request, provider.Item) {
matches = append(matches, provider)
}
}
if len(matches) == 0 {
return contribution.ContributionOpenProvider{}, fmt.Errorf("no open provider supports %s %q", request.Kind, request.Path)
}
preferred := r.preferenceFor(request)
if preferred != "" {
for _, provider := range matches {
if provider.Item.ID == preferred {
return provider, nil
}
}
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].Item.Priority != matches[j].Item.Priority {
return matches[i].Item.Priority > matches[j].Item.Priority
}
if matches[i].PluginID != matches[j].PluginID {
return matches[i].PluginID < matches[j].PluginID
}
return matches[i].Item.ID < matches[j].Item.ID
})
return matches[0], nil
}
func (r *Router) OpenResource(request OpenResourceRequest, providers []contribution.ContributionOpenProvider) (OpenResourceResult, error) {
request = normalizeRequest(request)
provider, err := r.SelectProvider(request, providers)
if err != nil {
return OpenResourceResult{
Status: "no-provider",
Request: request,
Message: err.Error(),
}, nil
}
result := OpenResourceResult{
Status: "opened",
ProviderID: provider.Item.ID,
ProviderPluginID: provider.PluginID,
ProviderComponent: provider.Item.Component,
Request: request,
}
r.opened = append(r.opened, OpenedResource{
ID: fmt.Sprintf("%s:%d", provider.Item.ID, len(r.opened)+1),
ProviderID: result.ProviderID,
ProviderPluginID: result.ProviderPluginID,
ProviderComponent: result.ProviderComponent,
Request: result.Request,
OpenedAt: time.Now().UTC().Format(time.RFC3339Nano),
})
return result, nil
}
func (r *Router) OpenedResources() []OpenedResource {
result := make([]OpenedResource, len(r.opened))
copy(result, r.opened)
return result
}
func normalizeRequest(request OpenResourceRequest) OpenResourceRequest {
if request.Mode == "" {
request.Mode = "view"
}
if request.Extension == "" {
request.Extension = path.Ext(request.Path)
}
request.Extension = strings.ToLower(request.Extension)
request.Mime = strings.ToLower(request.Mime)
return request
}
// DetermineContextName derives the current routing context from a request.
// Future Files/Notes callers can move canonical Notes folder auto-detection here.
func DetermineContextName(request OpenResourceRequest) string {
request = normalizeRequest(request)
return resourceContextName(request)
}
func providerMatches(request OpenResourceRequest, provider plugin.ContributionOpenProvider) bool {
for _, support := range provider.Supports {
if support.Kind != request.Kind {
continue
}
if !supportMatchesExtensionOrMime(request, support) {
continue
}
if !supportMatchesContext(request, support) {
continue
}
return true
}
return false
}
func supportMatchesExtensionOrMime(request OpenResourceRequest, support plugin.OpenProviderSupport) bool {
hasExtensionRules := len(support.Extensions) > 0
hasMimeRules := len(support.Mime) > 0
if !hasExtensionRules && !hasMimeRules {
return true
}
if hasExtensionRules {
for _, ext := range support.Extensions {
if strings.ToLower(ext) == request.Extension {
return true
}
}
}
if hasMimeRules && request.Mime != "" {
for _, mime := range support.Mime {
if strings.ToLower(mime) == request.Mime {
return true
}
}
}
return false
}
func supportMatchesContext(request OpenResourceRequest, support plugin.OpenProviderSupport) bool {
if len(support.Contexts) == 0 {
return true
}
context := resourceContextName(request)
for _, supported := range support.Contexts {
if supported == context {
return true
}
}
return false
}
func (r *Router) preferenceFor(request OpenResourceRequest) string {
context := resourceContextName(request)
switch {
case context == ContextNotesMarkdown:
return r.preferences.DefaultNotesMarkdownEditorProvider
case context == ContextGenericMarkdown:
return r.preferences.DefaultMarkdownEditorProvider
case context == ContextGenericText:
return r.preferences.DefaultTextEditorProvider
default:
return ""
}
}
func resourceContextName(request OpenResourceRequest) string {
ext := strings.ToLower(request.Extension)
if ext == ".md" || ext == ".markdown" {
if request.Context.NotesMode || request.Context.IsInsideNotesFolder {
return ContextNotesMarkdown
}
return ContextGenericMarkdown
}
if isTextResource(request) {
return ContextGenericText
}
return ""
}
func isTextResource(request OpenResourceRequest) bool {
if strings.HasPrefix(request.Mime, "text/") {
return true
}
switch strings.ToLower(request.Extension) {
case ".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".ini", ".conf":
return true
default:
return false
}
}