258 lines
7.5 KiB
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
|
|
}
|
|
}
|