-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add dev-doc folder and document some stuff there
- Loading branch information
Showing
5 changed files
with
260 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# Porcupine's Architecture and Design | ||
|
||
**TL;DR:** | ||
- core of Porcupine is small, and most of the functionality is in plugins | ||
- plugins are loaded dynamically | ||
- each plugin does a specific thing, and can be enabled/disabled individually by the user | ||
- plugins can depend on each other when needed, and they pass messages to each other through virtual events. | ||
|
||
|
||
## Small core, powerful plugins | ||
|
||
Porcupine's architecture is super simple. There is: | ||
- A **very minimal core** that provides only very basic functionality and can barely edit a text file. | ||
- Lots of **plugins** that use the core to implement various things. | ||
|
||
The core means everything except the plugins. | ||
|
||
Plugins are just files in the `porcupine/plugins/` directory. | ||
When Porcupine starts, it looks inside that directory and dynamically imports each file. | ||
It then calls a function named `setup()` in each plugin. | ||
(It is also possible to place plugins into one other directory, but most people should never need this feature.) | ||
|
||
Basically, if something can reasonably be a plugin, it most likely is a plugin in Porcupine. | ||
To get an idea of just how strongly Porcupine relies on plugins, | ||
you can run it with all plugins disabled: | ||
|
||
``` | ||
(env)$ python -m porcupine --no-plugins | ||
``` | ||
|
||
It will look something like this: | ||
|
||
![Screenshot of porcupine without plugins 1](no-plugins-1.png) | ||
|
||
![Screenshot of porcupine without plugins 2](no-plugins-2.png) | ||
|
||
|
||
## Pros and cons | ||
|
||
The plugin-heavy design has many advantages: | ||
- You don't usually need to read a lot of code to understand how something works. | ||
Instead, you study just one plugin, and at the time of writing this documentation, | ||
the average length of a plugin is only **167 lines**! | ||
- Finding the buggy code is easy in Porcupine. | ||
If feature X doesn't work, it is most likely caused by `porcupine/plugins/X.py`. | ||
- Most plugins are somewhat self-contained, so it's easy to understand how they work. | ||
- Users can use the plugin manager to disable features they don't like, | ||
even if Porcupine developers didn't add something specifically to disable a feature. | ||
|
||
And some disadvantages: | ||
- The loading order (that is, order of calling `setup()` functions) matters. | ||
- Some plugins need to communicate with each other, and it can get complicated. | ||
|
||
Basically, because so much functionality is implemented as plugins, | ||
the plugins depend on each other in various ways, and it can complicate things. | ||
It also just doesn't feel right to many people, and I understand that. | ||
|
||
That said, the plugin-heavy design works very well in practice. | ||
This plugin system has made Porcupine much more successful than it would have been otherwise. | ||
Even though there are lots of features, Porcupine is still very maintainable, | ||
because it's easy to ignore a lot of code you don't care about. | ||
|
||
|
||
## Loading Order | ||
|
||
By default, Porcupine invokes the `setup()` functions | ||
in alphabetical order when running normally, and in random order when running tests. | ||
|
||
To control the order, plugins can define global variables named `setup_before` and `setup_after`. | ||
Let's say you have `porcupine/plugins/foo.py` and `porcupine/plugins/bar.py`. | ||
By default, the order will be alphabetical: | ||
|
||
1. `bar.setup()` | ||
2. `foo.setup()` | ||
|
||
But if `foo.py` sets `setup_before = ["bar"]`, or if `bar.py` sets `setup_after = ["foo"]`, the order will be: | ||
|
||
1. `foo.setup()` | ||
2. `bar.setup()` | ||
|
||
For example, the `filemanager` plugin adds things like "New file here" and "New directory here" | ||
to the right-click menu you get from the directory tree on the left. | ||
To do this, it accesses the directory tree in its `setup()`, so the `filemanager` plugin contains this line: | ||
|
||
``` | ||
setup_after = ["directory_tree"] | ||
``` | ||
|
||
|
||
## Communicating Between Plugins | ||
|
||
Ideally, all plugins would be self-contained. | ||
Each plugin would do its own thing, and the plugins wouldn't know anything about each other. | ||
In practice, this isn't quite true. | ||
For example, the `langserver` plugin needs to interact with many parts of Porcupine, | ||
including several other plugins. | ||
|
||
There are basically two ways how plugins can communicate with each other: | ||
1. Importing a `get_foo()` function | ||
2. Virtual events | ||
|
||
A good example of importing a `get_foo()` function is the directory tree on the left. | ||
The `directory_tree` plugin defines a function `get_directory_tree()` that returns the directory tree widget. | ||
(It is an instance of a subclass of `tkinter.ttk.Treeview`.) | ||
Other plugins (such as `filemanager` and `git_status`) then add more functionality to the directory tree. | ||
To access it, they import the `get_directory_tree()` function from the `directory_tree` plugin and call it from `setup()`. | ||
|
||
Virtual events are Porcupine's (and tkinter's) way to do callbacks. | ||
If plugin A generates a virtual event on a widget, | ||
and plugin B binds to that virtual event (in its `setup()`, for example), | ||
then a callback function specified in plugin B will run whenever plugin A triggers the event. | ||
This is useful, because plugin A can run code in plugin B without knowing anything about plugin B. | ||
For more info about virtual events, see [virtual-events.md](virtual-events.md). | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
# Virtual Events | ||
|
||
If you are tempted to make a list of callback functions, expose it to another part of Porcupine, | ||
and then call each function in the list, you should probably use a virtual event instead. | ||
Here's how they work: | ||
|
||
```python | ||
def print_hi(event): | ||
print("hi") | ||
|
||
some_widget.bind("<<PrintHi>>", print_hi, add=True) | ||
some_widget.event_generate("<<PrintHi>>") # this prints hi | ||
``` | ||
|
||
You can name virtual events anything you want as long as you use the double `<<` and `>>`. | ||
There are some predefined virtual events, but you likely won't clash with them by accident. | ||
|
||
Virtual events are specific to a widget. | ||
Nothing will happen if you `.bind()` and `.event_generate()` on different widgets. | ||
This is useful for things that only affect one tab, for example. | ||
|
||
Make sure to spell virtual events the same way in all places. | ||
**You will get no errors if you misspell the name of a virtual event.** | ||
Your callbacks just won't run. | ||
|
||
|
||
## Multiple Callbacks | ||
|
||
(Actually, this section applies to all tkinter events, not just virtual events.) | ||
|
||
With `add=True`, you can bind multiple things to the same virtual event. | ||
This is almost always what you should do in Porcupine. | ||
For whatever reason, the default is `add=False`, which deletes all existing bindings. | ||
|
||
```python | ||
def print_hi(event): | ||
print("hi") | ||
|
||
def print_hello(event): | ||
print("hello") | ||
|
||
some_widget.bind("<<PrintHi>>", print_hi, add=True) | ||
some_widget.bind("<<PrintHi>>", print_hello, add=True) | ||
some_widget.event_generate("<<PrintHi>>") # prints hi and hello | ||
``` | ||
|
||
You can return the string `"break"` from a callback function to prevent running other | ||
callbacks that have been added afterwards. | ||
|
||
```python | ||
def print_hi(event): | ||
print("hi") | ||
return "break" # <--- This stops processing the event | ||
|
||
def print_hello(event): | ||
print("hello") | ||
|
||
some_widget.bind("<<PrintHi>>", print_hi, add=True) | ||
some_widget.bind("<<PrintHi>>", print_hello, add=True) | ||
some_widget.event_generate("<<PrintHi>>") # only prints hi | ||
``` | ||
|
||
This is typically used together with [setup_before and setup_after](archotecture-and-design.md#loading-order) | ||
to decide which plugin gets to handle a virtual event. | ||
|
||
|
||
## Passing Data | ||
|
||
It is possible to pass data when generating a virtual event | ||
and receive (a copy of) the data when the `.bind()` callback runs. | ||
See the docstring of the `porcupine.utils.bind_with_data()` function for details. | ||
You need to use a function from `porcupine.utils` because | ||
Tkinter doesn't expose this functionality very nicely, | ||
but it is needed often because Porcupine relies quite heavily on virtual events. | ||
|
||
|
||
## Simple Example: `<<Reloaded>>` | ||
|
||
**Source Code:** | ||
- [porcupine/tabs.py](../porcupine/tabs.py) (search for `<<Reloaded>>`) | ||
- [porcupine/plugins/mergeconflict.py](../porcupine/plugins/mergeconflict.py) | ||
|
||
A couple things need to happen when a file's content is read from disk and placed into a text widget. | ||
For example, if the file contains [Git merge conflicts](https://akuli.github.io/git-guide/branches.html#merges-and-merge-conflicts), | ||
the `mergeconflict` plugin will notice them and show a nice UI for resolving them. | ||
|
||
To implement this, we need some way to run code in a plugin when the core is done reloading a file's content. | ||
We obviously cannot import from `porcupine.plugins.mergeconflict` in the core, | ||
so we need some sort of callbacks, | ||
and because this is Porcupine, we use virtual events for the callbacks. | ||
|
||
Specifically, the `mergeconflict` plugin binds to `<<Reloaded>>` on each new tab. | ||
Here's how it works: | ||
1. Porcupine's core reads a file's content from disk and places into a text widget. | ||
2. Porcupine's core generates a `<<Reloaded>>` event. | ||
3. The `mergeconflict` plugin has done a `.bind("<<Reloaded>>", ...)`, | ||
so a function in the `mergeconflict` plugin runs. | ||
4. The `mergeconflict` plugin finds all merge conflicts in the file and does its thing. | ||
|
||
|
||
## Complex Example: jump to definition | ||
|
||
**Source Code:** | ||
- [porcupine/plugins/jump_to_definition.py](../porcupine/plugins/jump_to_definition.py) | ||
- [porcupine/plugins/langserver.py](../porcupine/plugins/langserver.py) (search for `jump_to_def`) | ||
- [porcupine/plugins/urls.py](../porcupine/plugins/urls.py) | ||
|
||
Let's say you have a line of Python code that looks like `self.do_something()`. | ||
If you bring the cursor to the middle of `do_something()` and press Ctrl+Enter, | ||
or if you control-click `do_something`, | ||
Porcupine will take you to a line of code that looks something like `def do_something(self):`. | ||
|
||
This is implemented in two different plugins: | ||
- The `langserver` plugin is responsible for finding the `def do_something(self):` line from your project. | ||
- The `jump_to_definition` plugin does everything else. | ||
|
||
The `jump_to_definition` plugin doesn't know anything about the langserver plugin. | ||
All it knows is that the user requests a jump-to-definition, | ||
and eventually it gets a response telling it where to go. | ||
This is great, because langserver isn't necessarily the only way to find out where something is defined. | ||
For example, if you have `https://example.com/` in your code and you Ctrl+Enter on it, | ||
that is *also* a jump-to-definition as far as Porcupine is concerned, | ||
although the "definition" is a website in this case and it opens in a browser window. | ||
|
||
More specifically, here's what happens when you press Ctrl+Enter on `self.do_something()`: | ||
|
||
1. The `jump_to_definition` sees the Ctrl+Enter. It generates a `<<JumpToDefinitionRequest>>` virtual event. | ||
2. The `urls` plugin has done a `.bind("<<JumpToDefinitionRequest>>", ...)`, but it ignores the event. | ||
2. The `langserver` plugin has done a `.bind("<<JumpToDefinitionRequest>>", ...)`, | ||
so a function in the `langserver` plugin runs. | ||
3. The `langserver` plugin figures out where the `def do_something(self):` line is. | ||
This will take a while. Porcupine doesn't freeze when this happens. | ||
4. The `langserver` plugin generates a `<<JumpToDefinitionResponse>>` virtual event. | ||
5. The `jump_to_definition` plugin has done a `.bind("<<JumpToDefinitionResponse>>")`, | ||
so it receives the response from the `langserver` plugin. | ||
6. The `jump_to_definition` plugin takes you to the `def do_something(self):` line. | ||
|
||
And here's what happens when you press Ctrl+Enter on a URL: | ||
|
||
1. The `jump_to_definition` sees the Ctrl+Enter. It generates a `<<JumpToDefinitionRequest>>` virtual event. | ||
2. The `urls` plugin has done a `.bind("<<JumpToDefinitionRequest>>", ...)`, | ||
so a function in the `urls` plugin runs. | ||
3. The `urls` plugin opens the URL in the default web browser. | ||
4. The `urls` plugin returns `"break"` to prevent the `langserver` plugin from handling the event. |