Far-Beyond-Pulsar/Pulsar-Native

(DRAFT) Plugin system updates

Open

#259 opened on Jun 16, 2026

View on GitHub
 (0 comments) (0 reactions) (2 assignees)Rust (29 forks)auto 404
CleanupSecuritydocumentationenhancementgood first issuehelp wanted

Repository metrics

Stars
 (348 stars)
PR merge metrics
 (PR metrics pending)

Description

Intro

I'll be adding more here soon as the scope of this overhaul to the API becomes more clear. for now most detail is in sub-issues

pub trait EditorPlugin Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn version(&self) -> &str;
    fn author(&self) -> &str;
    fn license(&self) -> &str;
    fn homepage(&self) -> Option<&str>;
    fn dependencies(&self) -> Option<&[Dependency]>;
}

pub trait EditorPluginFiles: EditorPlugin {
    /// Get all file types this plugin supports.
    fn file_types(&self) -> Vec<FileTypeDefinition>;

    /// Get all editor types this plugin provides.
    /// 
    /// The editors are each responsible for editing one or more file types.
    /// The editors are also responsible for defining any AI tools they wish
    /// to be registered with the AI chat system. All file type definitions
    /// are used to determine which editor to use for a given file and as
    /// those files are valid targets of their respective editors and the
    /// AI tools of those editors.
    fn editors(&self) -> Vec<EditorDefinition>;

    /// Create a new editor instance for the given file.
    fn create_editor(
        &self,
        editor_id: EditorId,
        file_path: PathBuf,
        window: &mut Window,
        cx: &mut App,
    ) -> Result<Arc<dyn PanelView>, PluginError>;

}

pub trait EditorPluginStatusBar: EditorPlugin {
    /// Get statusbar buttons this plugin wants to register.
    ///
    /// This is optional - plugins that don't need statusbar buttons can use the default implementation.
    ///
    /// # Returns
    ///
    /// A vector of statusbar button definitions. Return an empty vector if no buttons are needed.
    ///
    /// # Safety
    ///
    /// It is safe to return function pointers in `StatusbarButtonDefinition` because
    /// plugins are never unloaded. The function pointers will remain valid for the
    /// process lifetime.
    fn statusbar_buttons(&self) -> Vec<StatusbarButtonDefinition> {
        Vec::new()
    }

    /// Get any statusbar badges this plugin wants to display.
    ///
    /// This is optional - plugins that don't need statusbar badges can use the default implementation.
    ///
    /// # Returns
    ///
    /// A vector of statusbar badge definitions. Return an empty vector if no badges are needed.
    fn statusbar_badges(&self) -> Vec<StatusbarBadgeDefinition> {
        Vec::new()
    }
}

trait EditorPluginComponents: EditorPlugin {
    /// Returns all ComponentDefinitions for this plugin.
    /// e.g. how do we allow a plugin to create abd add components to the engiune such as: crates\pulsar_physics\src\components\physics_component/*
    fn component_definitions(&self) -> Vec<ComponentDefinition>;
}



/// Complete definition of a file type that a plugin supports.
#[derive(Debug, Clone)]
pub struct FileTypeDefinition {
    /// Unique identifier for this file type
    pub id: FileTypeId,

    /// File extension (without the dot, e.g., "rs" not ".rs")
    /// For folder-based files, this is the folder extension
    pub extension: String,

    /// Human-readable name for this file type
    pub display_name: String,

    /// Icon to show in the file drawer
    pub icon: ui::IconName,

    /// Color for the icon
    pub color: gpui::Hsla,

    /// Whether this is a standalone file or folder-based
    pub structure: FileStructure,

    /// Default content for new files (as JSON)
    /// For folder-based files, this is the content of the marker file
    pub default_content: serde_json::Value,

    /// Default Editor
    pub default_editor: EditorId,

    /// Optional category path for organizing in the create menu
    /// Examples: vec!["Data"], vec!["Data", "SQLite"], vec!["Scripts", "Web"]
    /// Leave empty for top-level menu items
    pub categories: Vec<String>,
}

/// Defines the structure of a file type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileStructure {
    /// A single standalone file (e.g., script.rs)
    Standalone {
        /// TemplateFile
        templates: Vec<TemplateFile>,
    },

    /// A folder that appears as a file in the drawer (e.g., MyClass.class/)
    /// Contains the marker file name that identifies this folder as this type
    /// For example, "graph_save.json" for blueprint classes
    FolderBased {
        /// The marker file that must exist in the folder
        marker_file: String,
        /// Additional files/folders that should be created in a new instance
        template_structure: Vec<PathTemplate>,

        /// The name of the folder to create
        templates: Vec<TemplateFolder>,
    },
}

pub struct TemplateFile {
    /// The name of the file to create
    pub name: String,
    /// The content of the file (as JSON)
    pub content: serde_json::Value,
}

pub struct TemplateFolder {
    /// The name of the folder to create
    pub name: String,
    /// The content of the folder (as JSON)
    pub content: serde_json::Value,
    /// Additional files/folders that should be created in the folder
    pub template_structure: Vec<PathTemplate>,
}

/// Template for creating files/folders in a folder-based file type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PathTemplate {
    /// Create a file with default content
    File { path: String, content: String },
    /// Create a folder
    Folder { path: String },
}


// ============================================================================
// Plugin Error
// ============================================================================

/// Errors that can occur in plugin operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PluginError {
    /// Failed to load file
    FileLoadError { path: PathBuf, message: String },

    /// Failed to save file
    FileSaveError { path: PathBuf, message: String },

    /// Invalid file format
    InvalidFormat { expected: String, message: String },

    /// Editor not found
    EditorNotFound { editor_id: EditorId },

    /// File type not supported
    UnsupportedFileType { file_type_id: FileTypeId },

    /// Version mismatch
    VersionMismatch {
        expected: VersionInfo,
        actual: VersionInfo,
    },

    /// Generic error
    Other { message: String },

    /// Filesystem access denied by FsContext sandbox.
    AccessDenied(String),
}

impl fmt::Display for PluginError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::FileLoadError { path, message } => {
                write!(f, "Failed to load file {:?}: {}", path, message)
            }
            Self::FileSaveError { path, message } => {
                write!(f, "Failed to save file {:?}: {}", path, message)
            }
            Self::InvalidFormat { expected, message } => {
                write!(f, "Invalid format (expected {}): {}", expected, message)
            }
            Self::EditorNotFound { editor_id } => {
                write!(f, "Editor not found: {}", editor_id)
            }
            Self::UnsupportedFileType { file_type_id } => {
                write!(f, "Unsupported file type: {}", file_type_id)
            }
            Self::VersionMismatch { expected, actual } => {
                write!(
                    f,
                    "Version mismatch: expected {:?}, got {:?}",
                    expected, actual
                )
            }
            Self::Other { message } => write!(f, "{}", message),
            Self::AccessDenied(reason) => write!(f, "Access denied: {}", reason),
        }
    }
}

/// Metadata describing an editor that a plugin provides.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditorMetadata {
    /// Unique identifier for this editor
    pub id: EditorId,

    /// Human-readable name for this editor (Displayed in tabs)
    pub display_name: String,

    /// AI Tooling
    pub ai_tooling: Option<AiTooling>,
}

/// AI Tooling configuration for an editor.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiTooling {
    //  Integrate with toolbelt_rs to allow registration of fns as AI tools via macros: https://github.com/Far-Beyond-Pulsar/ToolbeltRSc
}





// Arc<> is not at all FFI safe. Currently We have an issue in the plugin system where GPUI (WGPUI) *unavoidably* depends on Arc internally which cannot change.
// 
// The leak:
// 
// On frame a plugin is told "render me a frame of your UI" where it returns a heavily nested data structure including Arc data internally. This data is leaked
// across frames because when passed over FFI boundaries, the atomic counter disconnects creating a plugin side and engine side counter that never sync. This means
// when the engine is done its memory deallocates, the plugin side counter is never updated and the memory is leaked.

Contributor guide