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.
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