canopus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/docs/adr/001-runtime-gem-components.md +24 -0
- data/docs/adr/002-bounded-diff-engine.md +22 -0
- data/docs/adr/003-background-language-analysis.md +22 -0
- data/docs/adr/004-persistent-display-map.md +23 -0
- data/docs/adr/005-windows-runtime.md +27 -0
- data/docs/adr/README.md +36 -0
- data/docs/distribution.md +27 -0
- data/docs/lsp.md +38 -0
- data/docs/performance.md +40 -0
- data/docs/snippets.md +32 -0
- data/docs/vim.md +24 -0
- data/docs/workspace_edits.md +32 -0
- data/examples/native_smoke.rb +25 -0
- data/examples/plugins/word_count.rb +11 -0
- data/exe/canopus +19 -0
- data/lib/canopus/block_map.rb +19 -0
- data/lib/canopus/buffer.rb +293 -0
- data/lib/canopus/cli.rb +171 -0
- data/lib/canopus/controller.rb +507 -0
- data/lib/canopus/data_compat.rb +25 -0
- data/lib/canopus/display_map/line_builder.rb +54 -0
- data/lib/canopus/display_map/line_set.rb +8 -0
- data/lib/canopus/display_map/pending_line_set.rb +9 -0
- data/lib/canopus/display_map/summary.rb +23 -0
- data/lib/canopus/display_map/worker.rb +89 -0
- data/lib/canopus/display_map.rb +350 -0
- data/lib/canopus/display_point.rb +5 -0
- data/lib/canopus/editor/snippet_expandable.rb +281 -0
- data/lib/canopus/editor.rb +445 -0
- data/lib/canopus/error.rb +5 -0
- data/lib/canopus/fold_map.rb +87 -0
- data/lib/canopus/git/blame.rb +50 -0
- data/lib/canopus/git/commit.rb +7 -0
- data/lib/canopus/git/corrupt_object.rb +7 -0
- data/lib/canopus/git/diff.rb +131 -0
- data/lib/canopus/git/index.rb +94 -0
- data/lib/canopus/git/object_database.rb +45 -0
- data/lib/canopus/git/pack.rb +186 -0
- data/lib/canopus/git/repository.rb +313 -0
- data/lib/canopus/git/status.rb +74 -0
- data/lib/canopus/git/tree_entry.rb +7 -0
- data/lib/canopus/git.rb +15 -0
- data/lib/canopus/icon_theme.rb +43 -0
- data/lib/canopus/language/background_analysis/job.rb +12 -0
- data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
- data/lib/canopus/language/background_analysis.rb +280 -0
- data/lib/canopus/language/definition.rb +7 -0
- data/lib/canopus/language/document.rb +143 -0
- data/lib/canopus/language/symbol.rb +7 -0
- data/lib/canopus/language/syntax_worker.rb +91 -0
- data/lib/canopus/language.rb +42 -0
- data/lib/canopus/lazy_rope.rb +218 -0
- data/lib/canopus/lsp/client.rb +299 -0
- data/lib/canopus/lsp/error.rb +7 -0
- data/lib/canopus/lsp/future/subscription.rb +9 -0
- data/lib/canopus/lsp/future.rb +87 -0
- data/lib/canopus/lsp/protocol.rb +83 -0
- data/lib/canopus/lsp/server_error.rb +13 -0
- data/lib/canopus/lsp/timeout.rb +7 -0
- data/lib/canopus/lsp/transport.rb +123 -0
- data/lib/canopus/lsp.rb +19 -0
- data/lib/canopus/markdown.rb +69 -0
- data/lib/canopus/match_data_compat.rb +17 -0
- data/lib/canopus/multi_buffer.rb +266 -0
- data/lib/canopus/pane.rb +61 -0
- data/lib/canopus/patch/composite.rb +12 -0
- data/lib/canopus/patch/reload.rb +18 -0
- data/lib/canopus/patch.rb +27 -0
- data/lib/canopus/performance_recorder.rb +207 -0
- data/lib/canopus/plugins/api.rb +50 -0
- data/lib/canopus/plugins/isolated_runtime.rb +167 -0
- data/lib/canopus/plugins/local_runtime.rb +25 -0
- data/lib/canopus/plugins/permission_denied.rb +7 -0
- data/lib/canopus/plugins/registry.rb +30 -0
- data/lib/canopus/plugins.rb +11 -0
- data/lib/canopus/project/ignore_matcher.rb +104 -0
- data/lib/canopus/project/search.rb +164 -0
- data/lib/canopus/project/search_worker/cancelled.rb +3 -0
- data/lib/canopus/project/search_worker/runner.rb +9 -0
- data/lib/canopus/project/search_worker.rb +108 -0
- data/lib/canopus/project/tree.rb +48 -0
- data/lib/canopus/project/watcher.rb +59 -0
- data/lib/canopus/project.rb +127 -0
- data/lib/canopus/regexp_compat.rb +30 -0
- data/lib/canopus/save_conflict.rb +5 -0
- data/lib/canopus/selection.rb +11 -0
- data/lib/canopus/settings.rb +118 -0
- data/lib/canopus/snippet/transform.rb +209 -0
- data/lib/canopus/snippet.rb +183 -0
- data/lib/canopus/tab_map.rb +30 -0
- data/lib/canopus/terminal/cell.rb +7 -0
- data/lib/canopus/terminal/grid.rb +321 -0
- data/lib/canopus/terminal/pty.rb +103 -0
- data/lib/canopus/terminal/scrollback.rb +38 -0
- data/lib/canopus/terminal/vt.rb +398 -0
- data/lib/canopus/terminal.rb +13 -0
- data/lib/canopus/theme.rb +43 -0
- data/lib/canopus/version.rb +5 -0
- data/lib/canopus/vim/commandable.rb +148 -0
- data/lib/canopus/vim/motionable.rb +321 -0
- data/lib/canopus/vim/operator_capable.rb +377 -0
- data/lib/canopus/vim/text_object_selectable.rb +141 -0
- data/lib/canopus/vim.rb +426 -0
- data/lib/canopus/workspace/edit/executable.rb +139 -0
- data/lib/canopus/workspace/edit/node.rb +8 -0
- data/lib/canopus/workspace/edit/plan.rb +170 -0
- data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
- data/lib/canopus/workspace/edit.rb +6 -0
- data/lib/canopus/workspace/file_change_aware.rb +40 -0
- data/lib/canopus/workspace/file_previewable.rb +31 -0
- data/lib/canopus/workspace/git_aware.rb +140 -0
- data/lib/canopus/workspace/language_aware.rb +413 -0
- data/lib/canopus/workspace/language_server_configurable.rb +181 -0
- data/lib/canopus/workspace/project_searchable.rb +208 -0
- data/lib/canopus/workspace/project_tree_editable.rb +82 -0
- data/lib/canopus/workspace/session_persistable.rb +153 -0
- data/lib/canopus/workspace/settings_aware.rb +91 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
- data/lib/canopus/workspace/view.rb +643 -0
- data/lib/canopus/workspace.rb +525 -0
- data/lib/canopus/wrap_map.rb +108 -0
- data/lib/canopus.rb +24 -0
- data/sig/canopus.rbs +375 -0
- data/sig/controller.rbs +55 -0
- data/sig/display_stages.rbs +49 -0
- data/sig/git.rbs +140 -0
- data/sig/lsp.rbs +99 -0
- data/sig/markdown.rbs +11 -0
- data/sig/performance_recorder.rbs +23 -0
- data/sig/search_services.rbs +8 -0
- data/sig/services.rbs +81 -0
- data/sig/snippet.rbs +42 -0
- data/sig/terminal.rbs +125 -0
- data/sig/vim.rbs +27 -0
- data/sig/workspace_edits.rbs +11 -0
- data/sig/workspace_services.rbs +117 -0
- data/tools/certify_snippet_regex.rb +35 -0
- data/tools/check_dependencies.rb +80 -0
- data/tools/native_check.rb +70 -0
- data/tools/package.rb +113 -0
- data/tools/package_test.rb +32 -0
- metadata +314 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b3f1beb3dd3a5647f1ac04e7fc710356c5a90d4d8c3cefa01d676c463bd95950
|
|
4
|
+
data.tar.gz: 5e4b81d47c4c442b3f74b4e87745d0c8e7e06b3a4119ee845205fd6c1628f22a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ecb15f56e99c82bc6b7d7a1368096d5d4db4ea53b9993ea2ad3cd804ea5cd61fb6ca33a4470856d89a0e4ae22004d7876df992f8233a26e0864e2da27fbf5c71
|
|
7
|
+
data.tar.gz: 91e0d07140b34dda056874ce090bc37507a055f8f8685e5678c458cdfe74dd7d8137598793c2c8fb11152828de307f2f2b7e85040719c85b80bc6f3bce8d0cc9
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Canopus
|
|
2
|
+
|
|
3
|
+
A Ruby-native code editor built on Zaniah: persistent UTF-8 buffers, native GPU
|
|
4
|
+
windows, language-server tools, a terminal, Git inspection, editable project
|
|
5
|
+
search results, and Vim bindings.
|
|
6
|
+
|
|
7
|
+
The launcher requires CRuby 3.1+. It attempts to enable YJIT on macOS/Linux
|
|
8
|
+
and falls back to ordinary Ruby when unavailable; Windows uses ordinary Ruby
|
|
9
|
+
([runtime decision](docs/adr/005-windows-runtime.md)). OS libraries use Fiddle; language
|
|
10
|
+
servers and the terminal shell are user-installed programs. Ruby libraries are
|
|
11
|
+
installed as gem dependencies; Prism is bundled with recent Ruby releases.
|
|
12
|
+
|
|
13
|
+
Native windows need `require "fiddle"` to work in the chosen Ruby installation.
|
|
14
|
+
On Ruby distributions that package it separately (including the Ruby 4.0 build
|
|
15
|
+
used here), install that standard-library component first. Development Bundler
|
|
16
|
+
configuration includes it; headless rendering does not load it.
|
|
17
|
+
|
|
18
|
+
## Run from this checkout
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bundle install
|
|
22
|
+
bundle exec ruby exe/canopus --project . README.md
|
|
23
|
+
ruby exe/canopus --headless /tmp/canopus.png README.md
|
|
24
|
+
ruby exe/canopus --tui README.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--help` lists backend, session, settings, replay, cache, and plugin options.
|
|
28
|
+
macOS uses Metal, Linux uses Wayland/EGL or X11/GLX, and Windows uses WGL.
|
|
29
|
+
|
|
30
|
+
Install a local artifact with:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
gem build canopus.gemspec
|
|
34
|
+
gem install --local canopus-0.1.0.gem
|
|
35
|
+
canopus --project .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See [packaging](docs/distribution.md) for desktop bundles and platform
|
|
39
|
+
requirements.
|
|
40
|
+
|
|
41
|
+
## Everyday controls
|
|
42
|
+
|
|
43
|
+
| Action | macOS | Linux / Windows |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| File finder / commands | Cmd-P / Cmd-Shift-P | Ctrl-P / Ctrl-Shift-P |
|
|
46
|
+
| Save / undo / redo | Cmd-S / Cmd-Z / Cmd-Shift-Z | Ctrl-S / Ctrl-Z / Ctrl-Shift-Z |
|
|
47
|
+
| Find / project search | Cmd-F / Cmd-Shift-F | Ctrl-F / Ctrl-Shift-F |
|
|
48
|
+
| Replace / next occurrence | Cmd-Alt-F / Cmd-D | Ctrl-H / Ctrl-D |
|
|
49
|
+
| All occurrences / move lines | Cmd-Shift-L / Alt-Up or Down | Ctrl-Shift-L / Alt-Up or Down |
|
|
50
|
+
| Completion / definition | Ctrl-Space / F12 | Ctrl-Space / F12 |
|
|
51
|
+
| Split / terminal | Cmd-Backslash / Ctrl-Backtick | Ctrl-Backslash / Ctrl-Backtick |
|
|
52
|
+
|
|
53
|
+
The palette exposes Git diff/blame/hunk reversal/branch switching, language
|
|
54
|
+
actions, project file operations, settings, themes, [Vim mode](docs/vim.md), and docks. Drag tabs
|
|
55
|
+
to reorder or move them between panes; drag separators to resize panes and docks.
|
|
56
|
+
Project deletion moves files into `.canopus/trash`. Dirty documents and disk
|
|
57
|
+
save conflicts are preserved for an explicit decision.
|
|
58
|
+
|
|
59
|
+
The file finder keeps recent files first and previews text without opening tabs.
|
|
60
|
+
Completion supports additional edits and [snippets](docs/snippets.md), including
|
|
61
|
+
mirrors, transforms, choices, and Tab / Shift-Tab navigation. Hover cards render
|
|
62
|
+
bounded Markdown; only explicitly clicked HTTP(S) links open externally.
|
|
63
|
+
Notifications are dismissible and expire automatically.
|
|
64
|
+
|
|
65
|
+
Project-search results are editable; Save writes changes back to their source
|
|
66
|
+
files. Replacement changes remain unsaved until then.
|
|
67
|
+
|
|
68
|
+
Search palettes use Ctrl-Alt-R for regular expressions, Ctrl-Alt-C for case sensitivity,
|
|
69
|
+
Ctrl-Alt-W for whole words and Ctrl-Alt-S for the selection captured when search opened.
|
|
70
|
+
Replacements support Ruby backreferences, and stale selections are rejected.
|
|
71
|
+
Resource-changing LSP actions show the affected paths before confirmation;
|
|
72
|
+
PageUp / PageDown scroll the list. See [workspace edits](docs/workspace_edits.md).
|
|
73
|
+
|
|
74
|
+
## Settings and extensions
|
|
75
|
+
|
|
76
|
+
`settings.open` opens `.canopus/settings.jsonc`. User settings live at
|
|
77
|
+
`$XDG_CONFIG_HOME/canopus/settings.jsonc` or
|
|
78
|
+
`~/.config/canopus/settings.jsonc`. Layers are defaults, user, project, explicit
|
|
79
|
+
`--settings`, then language overrides. Saved changes reload; invalid values
|
|
80
|
+
preserve the previous valid settings. Ctrl-Space completes setting names.
|
|
81
|
+
|
|
82
|
+
```jsonc
|
|
83
|
+
{
|
|
84
|
+
"theme": "auto",
|
|
85
|
+
"font_size": 14,
|
|
86
|
+
"icon_theme": null,
|
|
87
|
+
"vim_mode": false,
|
|
88
|
+
"use_tabs": false,
|
|
89
|
+
"keymap": [
|
|
90
|
+
{ "context": "Editor && !vim_mode", "bindings": { "ctrl-k ctrl-s": "file.save" } }
|
|
91
|
+
],
|
|
92
|
+
"languages": { "ruby": { "tab_size": 2 } },
|
|
93
|
+
"language_servers": { "ruby": ["ruby-lsp"] }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [language servers](docs/lsp.md) for server-specific options and reload
|
|
98
|
+
behavior.
|
|
99
|
+
|
|
100
|
+
Keymap groups extend the defaults; later matching bindings win. Space-separated
|
|
101
|
+
keys form a chord with a one-second timeout, and `null` removes a default action
|
|
102
|
+
binding. Contexts support `Editor`, `vim_mode`, comparisons and boolean operators,
|
|
103
|
+
not Ruby evaluation. Language-specific `keymap` arrays replace the global array.
|
|
104
|
+
Saved keymaps reload atomically, including palette labels; invalid expressions
|
|
105
|
+
leave the previous settings intact. Limits are 128 groups and 1,024 bindings.
|
|
106
|
+
|
|
107
|
+
An icon theme is a JSONC file with `file`, `directory`, `expanded_directory`, and
|
|
108
|
+
an `extensions` map (for example `".rb": "ruby.svg"`). SVG paths are relative to
|
|
109
|
+
that theme file and must remain inside its directory. Only the supported static
|
|
110
|
+
SVG subset is drawn; scripts, external references and entities are not executed.
|
|
111
|
+
|
|
112
|
+
Plugins require explicit trust:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
ruby exe/canopus --plugin examples/plugins/word_count.rb \
|
|
116
|
+
--trust-plugins --grant read_buffer
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The default separate process contains crashes and timeouts. It is **not an OS
|
|
120
|
+
security sandbox**: trusted Ruby retains the invoking user's OS privileges.
|
|
121
|
+
API grants restrict the editor bridge, not arbitrary Ruby filesystem/network
|
|
122
|
+
access. `--plugins-in-process` opts into direct execution.
|
|
123
|
+
|
|
124
|
+
## Development and limits
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
bundle exec rake test
|
|
128
|
+
bundle exec rake bench
|
|
129
|
+
bundle exec ruby tools/check_dependencies.rb test/type/smoke.rb
|
|
130
|
+
bundle exec rbs -I sig -r alhena -r antares -r denebola -r zaniah -r stringio -r strscan validate
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Components are Zaniah, Alhena, Denebola, Spica, Kochab and Antares.
|
|
134
|
+
Rouge supplies lexers; REXML parses static SVG; unicode-display_width and
|
|
135
|
+
unicode-emoji supply terminal-cell tables. They are installed from RubyGems.
|
|
136
|
+
|
|
137
|
+
Files over 100MiB use a bounded-cache UTF-8 read-only path without wrapping or
|
|
138
|
+
folding. Large-file UTF-16/legacy encodings are not supported there. Rouge
|
|
139
|
+
lexers that cannot resume exactly use a documented bounded-window fallback,
|
|
140
|
+
which can approximate long-distance syntax state. Git supports SHA-1 repositories,
|
|
141
|
+
not every Git index/object extension. See the [architecture decisions](docs/adr).
|
|
142
|
+
|
|
143
|
+
Opt-in [profiling and local crash reports](docs/performance.md) expose real
|
|
144
|
+
frame times and allocations without uploading project data.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT; see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ADR 001: Consume released components as runtime gems
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-11
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Canopus uses components maintained and released from independent repositories.
|
|
9
|
+
It can load them from sibling checkouts, copy their sources into this repository,
|
|
10
|
+
combine the repositories, or depend on their released gems. An installed Canopus
|
|
11
|
+
gem must not depend on the development directory layout.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Keep each component repository as its source and release boundary. Consume its
|
|
16
|
+
released gem as a runtime dependency without copying component sources into the
|
|
17
|
+
Canopus repository or locating sibling checkouts.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
|
|
21
|
+
Canopus packages stay small and component fixes follow normal gem releases.
|
|
22
|
+
Installations require access to compatible component versions, and coordinated
|
|
23
|
+
changes may need multiple releases. Revisit this decision if components can no
|
|
24
|
+
longer provide compatible releases independently.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR 002: Use one bounded diff engine
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-10
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Git diffs must have predictable memory use and stable results for large or
|
|
9
|
+
repetitive files. Canopus can use a general-purpose diff dependency, switch
|
|
10
|
+
algorithms by input size, or apply one bounded implementation to every input.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Use the linear-space Myers implementation for every Git diff instead of
|
|
15
|
+
switching algorithms by input size.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
Memory behavior and tie-breaking remain consistent across file sizes, but
|
|
20
|
+
Canopus owns the implementation and its performance limits. Revisit this
|
|
21
|
+
decision if a maintained implementation provides the same bounds and stable
|
|
22
|
+
results with less maintenance.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ADR 003: Isolate background language analysis in processes
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-10
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Language analysis must not block editing, install stale results, or require
|
|
9
|
+
`fork`, which is unavailable on native Windows. Work can run in threads sharing
|
|
10
|
+
live objects or in portable child processes exchanging serializable data.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Run named language-analysis handlers in bounded `Process.spawn` workers that
|
|
15
|
+
exchange framed JSON. Install results on the foreground tick only when their
|
|
16
|
+
document generation still matches.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
Analysis is portable, cancellable, and isolated from mutable editor objects.
|
|
21
|
+
Messages cannot carry live Ruby objects and require serialization. Revisit this
|
|
22
|
+
decision if process startup or serialization prevents interactive performance.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ADR 004: Store display transforms in one persistent summary tree
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-10
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Folding, tab expansion, wrapping, and block inlays all transform buffer
|
|
9
|
+
positions into displayed positions. Canopus can maintain separate mutable maps
|
|
10
|
+
for each transform or combine their ordered summaries in one persistent tree.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Represent Fold, Tab, Wrap, and Block transforms in one persistent summary tree.
|
|
15
|
+
Keep hit testing and edits tied to exact buffer ranges while pixel reflow runs
|
|
16
|
+
asynchronously.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
Transforms share one source of positional truth and snapshots remain cheap.
|
|
21
|
+
Updates and queries depend on the combined summary invariants, and asynchronous
|
|
22
|
+
reflow can briefly show provisional layout. Revisit this decision if transforms
|
|
23
|
+
stop being line-local or the combined tree becomes a measured bottleneck.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ADR 005: Run native Windows without requiring YJIT
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-10
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The editor must run on macOS, Linux, and native Windows. Requiring YJIT on every
|
|
9
|
+
platform conflicts with CRuby builds that do not support it on Windows. The
|
|
10
|
+
alternatives are to reject those Windows runtimes, introduce a different JIT,
|
|
11
|
+
or allow the supported CRuby interpreter while retaining the same functional
|
|
12
|
+
and performance expectations.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Require CRuby 3.1 or newer. Attempt to enable YJIT on macOS and Linux, falling
|
|
17
|
+
back to the interpreter when the selected Ruby build lacks it. On native Windows,
|
|
18
|
+
preserve an already enabled JIT but do not pass an unsupported YJIT flag.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
The application can start on native Windows without maintaining another JIT or
|
|
23
|
+
excluding the platform. Runtime performance now varies by Ruby build, so results
|
|
24
|
+
must identify the engine and JIT state. This does not relax the performance or
|
|
25
|
+
native-platform validation requirements. Revisit the decision when official
|
|
26
|
+
Windows YJIT support is available or interpreter performance cannot satisfy the
|
|
27
|
+
budgets.
|
data/docs/adr/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Architecture decision records
|
|
2
|
+
|
|
3
|
+
This directory records durable implementation choices made between credible
|
|
4
|
+
alternatives. Operational workflow, release policy, command syntax, detailed
|
|
5
|
+
formats, test evidence, benchmark results, and migration procedures belong in
|
|
6
|
+
the relevant reference or design document.
|
|
7
|
+
|
|
8
|
+
ADR filenames and headings use a gap-free three-digit sequence beginning with
|
|
9
|
+
`001`. Create new records with this template:
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
# ADR NNN: Implementation decision title
|
|
13
|
+
|
|
14
|
+
- Status: Proposed
|
|
15
|
+
- Date: YYYY-MM-DD
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
Describe the concrete implementation question and its compatibility, data,
|
|
20
|
+
runtime, or component constraints. Operational workflow and release policy do
|
|
21
|
+
not belong in this directory; see [the scope](README.md).
|
|
22
|
+
|
|
23
|
+
Name the credible alternatives. If there was no meaningful alternative, record
|
|
24
|
+
the behavior in reference documentation instead of creating an ADR.
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
Describe the durable boundary or architecture choice. Leave command syntax,
|
|
29
|
+
field-by-field formats, test evidence, benchmark results, and migration steps
|
|
30
|
+
in their authoritative documents.
|
|
31
|
+
|
|
32
|
+
## Consequences
|
|
33
|
+
|
|
34
|
+
Describe the important positive and negative trade-offs, including what would
|
|
35
|
+
make this decision worth revisiting.
|
|
36
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Packaging
|
|
2
|
+
|
|
3
|
+
`tools/package.rb` creates a source-based macOS app, Linux launcher and desktop
|
|
4
|
+
file, or Windows launcher and shortcut installer. The package includes Canopus
|
|
5
|
+
sources and assets, but not Ruby, native binaries, or gem dependencies.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
ruby tools/package.rb --platform mac --output /tmp/Canopus.app --ruby /path/to/ruby
|
|
9
|
+
ruby tools/package.rb --platform linux --output /tmp/canopus-linux --ruby /usr/bin/ruby
|
|
10
|
+
ruby tools/package.rb --platform windows --output /tmp/canopus-windows --ruby ruby.exe
|
|
11
|
+
ruby tools/package_test.rb
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The output path must not exist and must be outside the source tree. Build for the
|
|
15
|
+
Ruby executable available on the destination machine. Canopus requires CRuby
|
|
16
|
+
3.1 or newer and its runtime gems installed for that Ruby. The installed package
|
|
17
|
+
does not require Bundler.
|
|
18
|
+
|
|
19
|
+
macOS's system Ruby is generally too old. Linux requires its Wayland/EGL/OpenGL
|
|
20
|
+
or X11/GLX libraries, XKB, and `zenity` for file dialogs. Move a Linux package to
|
|
21
|
+
its final location before installing its generated desktop file. On Windows, run
|
|
22
|
+
`Create-Shortcut.ps1` from the package to create a shortcut.
|
|
23
|
+
|
|
24
|
+
These packages are unsigned and do not bundle security updates for Ruby. Signing,
|
|
25
|
+
notarization, automatic updates, and a relocatable Ruby runtime require separate
|
|
26
|
+
platform-specific release work. See [ADR 005](adr/005-windows-runtime.md) for the
|
|
27
|
+
Windows/YJIT runtime decision.
|
data/docs/lsp.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Language servers
|
|
2
|
+
|
|
3
|
+
Canopus uses separately installed language servers over LSP 3.17. Configure a
|
|
4
|
+
server with an argument array or an options object:
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"language_servers": {
|
|
9
|
+
"ruby": {
|
|
10
|
+
"command": ["ruby-lsp"],
|
|
11
|
+
"env": {"RUBYOPT": null},
|
|
12
|
+
"initialization_options": {},
|
|
13
|
+
"configuration": {"rubyLsp": {"enabledFeatures": {"diagnostics": true}}}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`languages.<language>.language_servers.<language>` overrides the global entry.
|
|
20
|
+
An absent or `null` entry auto-detects the language's default executable; it does
|
|
21
|
+
not disable the server. If no executable is found, Canopus leaves that language
|
|
22
|
+
without a server. Invalid settings keep the previous working connection.
|
|
23
|
+
|
|
24
|
+
Changes to the command, environment, or initialization options restart the
|
|
25
|
+
connection and reopen its documents. A configuration-only change sends
|
|
26
|
+
`workspace/didChangeConfiguration` without restarting. Resource-changing server
|
|
27
|
+
requests require confirmation as described in [workspace edits](workspace_edits.md).
|
|
28
|
+
|
|
29
|
+
Editor positions use UTF-8 byte offsets internally and are converted to LSP
|
|
30
|
+
UTF-16 positions. Servers selecting another position encoding are rejected.
|
|
31
|
+
Messages are limited to 32 MiB, crashes fail pending requests, and a connection
|
|
32
|
+
is restarted at most three times.
|
|
33
|
+
|
|
34
|
+
Developers can test installed servers with:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
ruby script/lsp_interop.rb ruby rust typescript go
|
|
38
|
+
```
|
data/docs/performance.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Diagnostics
|
|
2
|
+
|
|
3
|
+
Profiling and crash reports are opt-in:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
ruby --yjit exe/canopus --profile /tmp/canopus-profile.json example.rb
|
|
7
|
+
ruby --yjit exe/canopus --profile /tmp/canopus-allocations.json \
|
|
8
|
+
--trace-allocations example.rb
|
|
9
|
+
ruby exe/canopus --crash-report /tmp/canopus-crash.json example.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`--profile` records bounded frame timings, Ruby allocation counts, stack samples,
|
|
13
|
+
runtime details, and native draw counts when available. `--trace-allocations`
|
|
14
|
+
also summarizes locations of live traced objects, but can substantially increase
|
|
15
|
+
memory use and frame time. Missing native counters are reported as `null`.
|
|
16
|
+
|
|
17
|
+
`--crash-report` writes only after a handled Ruby exception. It includes a bounded
|
|
18
|
+
message, backtrace, cause chain, and runtime/rendering context. It does not include
|
|
19
|
+
environment variables, command-line arguments, buffers, project files,
|
|
20
|
+
screenshots, or arbitrary object values, and nothing is uploaded. Messages and
|
|
21
|
+
backtraces can still contain local paths or sensitive text, so review reports
|
|
22
|
+
before sharing them. Native crashes and process termination cannot reliably be
|
|
23
|
+
captured by this hook.
|
|
24
|
+
|
|
25
|
+
Use an existing private directory and a distinct filename for each report.
|
|
26
|
+
Symlinks and paths overlapping selected input or configuration files are rejected.
|
|
27
|
+
On macOS and Linux reports request mode `0600`; Windows users should choose a
|
|
28
|
+
private directory with appropriate ACLs.
|
|
29
|
+
|
|
30
|
+
Developers can produce current, reproducible measurements with:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ruby --yjit bench/frame_profile.rb --frames 120 --output /tmp/frames.json
|
|
34
|
+
ruby bench/startup.rb --runs 3
|
|
35
|
+
ruby --yjit tools/native_check.rb --idle /tmp/native-check.png
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Compare results only when the platform, renderer, viewport, Ruby/JIT mode,
|
|
39
|
+
warmup, and diagnostic options match. Headless software rendering is a regression
|
|
40
|
+
check, not a hardware-GPU performance result.
|
data/docs/snippets.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Snippets
|
|
2
|
+
|
|
3
|
+
Canopus accepts [LSP/VS Code snippet syntax](https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax),
|
|
4
|
+
including numbered tab stops, nested defaults, mirrors, choices, variables, and
|
|
5
|
+
placeholder transforms. Parsing never executes shell commands, Ruby, or
|
|
6
|
+
JavaScript.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
${1:name} = ${2|value,nil|}$0
|
|
10
|
+
${1/(.*)/${1:/upcase}/}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Tab and Shift-Tab move between fields. Mirrors update together, leaving a field
|
|
14
|
+
applies its transforms, and `$0` ends the session. If `$0` is omitted, Canopus
|
|
15
|
+
adds a final caret. Undo, redo, or starting another snippet ends the current
|
|
16
|
+
session. Each cursor expands its own snippet and selected-text variables.
|
|
17
|
+
|
|
18
|
+
Standard filename, workspace, selection, date/time, random, UUID, clipboard, and
|
|
19
|
+
language-comment variables are available. Unknown bare variables become editable
|
|
20
|
+
placeholders; known but unavailable variables use their default or an empty
|
|
21
|
+
string.
|
|
22
|
+
|
|
23
|
+
Transforms support the portable Ruby/JavaScript regular-expression overlap and
|
|
24
|
+
flags `g`, `i`, `m`, `s`, and `u`. Replacement formats include capture references,
|
|
25
|
+
case conversion, and conditional/default forms. JavaScript-only features such as
|
|
26
|
+
Unicode property escapes and regex backreferences are rejected rather than
|
|
27
|
+
silently changed. Ruby code and replacement text are never evaluated.
|
|
28
|
+
|
|
29
|
+
Snippet source is limited to 1 MiB, expanded text and individual transform output
|
|
30
|
+
to 4 MiB, nesting to 32 levels, and regular-expression execution to 50 ms. A
|
|
31
|
+
navigation step has a combined 250 ms transform deadline. Exceeding a limit raises
|
|
32
|
+
an error before transformed edits are committed.
|
data/docs/vim.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Vim mode
|
|
2
|
+
|
|
3
|
+
Set `"vim_mode": true` or use the command palette to enable modal editing. Canopus
|
|
4
|
+
implements its own Vim-compatible command parser; it does not launch Vim.
|
|
5
|
+
|
|
6
|
+
Modes include Normal, Insert, Replace, Visual, Visual Line, and Visual Block.
|
|
7
|
+
Supported motions include character, word/WORD, line, paragraph, matching-pair,
|
|
8
|
+
find-character, mark, and page motions. Counts compose with operators.
|
|
9
|
+
|
|
10
|
+
Operators include delete, change, yank, shift, reindent, case conversion, and
|
|
11
|
+
swap case. Text objects cover words, sentences, paragraphs, nested brackets,
|
|
12
|
+
quotes, and tags. Visual-block operations use display cells, preserving partial
|
|
13
|
+
tabs and wide Unicode glyphs. Undo/redo, repeat, joins, paste, registers, marks,
|
|
14
|
+
macros, search, and substitution are supported.
|
|
15
|
+
|
|
16
|
+
The Ex subset includes line addresses, `w [path]`, `reg`, tab/shift width and
|
|
17
|
+
expand-tab options, relative numbers, and `s` with `g` and `i`. Other Ex commands
|
|
18
|
+
are forwarded to workspace commands.
|
|
19
|
+
|
|
20
|
+
Search and substitution use Ruby regular expressions, not Vim's magic dialect.
|
|
21
|
+
Vimscript, arbitrary shell commands, and user mappings are not interpreted.
|
|
22
|
+
Clipboard registers `+` and `*` are in-memory registers rather than OS clipboard
|
|
23
|
+
integrations. Counts are capped at 10,000; macro playback allows 10 nested calls
|
|
24
|
+
and 10,000 keys.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Language-server file operations
|
|
2
|
+
|
|
3
|
+
Language servers can request ordered file creation, rename, deletion, and text
|
|
4
|
+
edits. Canopus shows every resource-changing request before writing anything.
|
|
5
|
+
Apply and Cancel are separate choices, resource operations default to Cancel,
|
|
6
|
+
and cancelling reports failure to the server without changing files.
|
|
7
|
+
|
|
8
|
+
Before confirmation, Canopus validates the complete operation sequence, including
|
|
9
|
+
versions and paths produced by earlier operations in the same request. Text edits
|
|
10
|
+
remain normal unsaved buffer changes. Renames preserve open buffers; filesystem
|
|
11
|
+
operations are not part of text undo history.
|
|
12
|
+
|
|
13
|
+
Safety rules:
|
|
14
|
+
|
|
15
|
+
- Targets must be local paths inside the project. Traversal through parent
|
|
16
|
+
symlinks, special files, Git metadata, and the recovery directory is rejected.
|
|
17
|
+
- Deletes and overwrites reject dirty or read-only documents. Rename destinations
|
|
18
|
+
must not collide with another open buffer.
|
|
19
|
+
- Parent directories must already exist or be created earlier in the same request.
|
|
20
|
+
Deleting a nonempty directory requires an explicit recursive operation.
|
|
21
|
+
- A request is limited to 10,000 document operations, 100,000 filesystem entries,
|
|
22
|
+
100,000 text edits, and 32 MiB of inserted text.
|
|
23
|
+
|
|
24
|
+
Deleted files and overwritten destinations move to `.canopus/trash/lsp-*` with
|
|
25
|
+
their original contents and permissions. Backups are not deleted automatically.
|
|
26
|
+
If an operation fails, Canopus attempts to reverse completed moves and places new
|
|
27
|
+
files in recovery rather than deleting them permanently.
|
|
28
|
+
|
|
29
|
+
This is not an operating-system transaction. Concurrent external changes,
|
|
30
|
+
permission failures, process termination, or callback errors can prevent complete
|
|
31
|
+
rollback. Recovery never overwrites an unexpected destination; inspect the
|
|
32
|
+
reported recovery paths before restoring files.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Development checkout: sibling gem paths are used only by this example.
|
|
4
|
+
root = File.expand_path("../..", __dir__)
|
|
5
|
+
%w[denebola zaniah alhena kochab spica antares canopus].each do |name|
|
|
6
|
+
$LOAD_PATH.unshift(File.join(root, name, "lib"))
|
|
7
|
+
end
|
|
8
|
+
require "canopus"
|
|
9
|
+
|
|
10
|
+
workspace = Canopus::Workspace.new(root: File.expand_path("..", __dir__))
|
|
11
|
+
workspace.open("lib/canopus.rb")
|
|
12
|
+
window = Zaniah::Platform.open_window(backend: :mac, gpu: ARGV.include?("--gl") ? :opengl : :metal, width: 800, height: 500, title: "Canopus native integration")
|
|
13
|
+
window.text_system = Zaniah::TextSystem::Renderer.new
|
|
14
|
+
controller = Canopus::Controller.new(workspace, window)
|
|
15
|
+
controller.tick
|
|
16
|
+
workspace.split(:horizontal)
|
|
17
|
+
controller.tick
|
|
18
|
+
path = ARGV.find { |arg| arg.end_with?(".png") } || File.join(Dir.tmpdir, "canopus-native.png")
|
|
19
|
+
window.write_png(path)
|
|
20
|
+
raise "native scene was not submitted" unless window.device.draw_calls.positive?
|
|
21
|
+
raise "split editor not rendered" unless controller.view.editor_bounds.length == 2
|
|
22
|
+
puts "#{window.device.class}: #{window.device.draw_calls} draws, #{path}"
|
|
23
|
+
workspace.close
|
|
24
|
+
window.on_close { true }
|
|
25
|
+
window.close
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Requires explicit trust plus the read_buffer permission. Works in either mode.
|
|
4
|
+
register_action("sample.word_count", description: "Count words in the current buffer") do |api|
|
|
5
|
+
text = api.text
|
|
6
|
+
api.notify("#{text.scan(/\S+/).length} words, #{text.length} characters")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
register_panel("Word count", side: :right) do |api|
|
|
10
|
+
"Words: #{api.text.scan(/\S+/).length}"
|
|
11
|
+
end
|
data/exe/canopus
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "rubygems"
|
|
6
|
+
unless RUBY_ENGINE == "ruby" && (RUBY_VERSION.split(".").first(2).map(&:to_i) <=> [3, 1]) >= 0
|
|
7
|
+
abort "Canopus requires CRuby 3.1 or newer."
|
|
8
|
+
end
|
|
9
|
+
unless RUBY_PLATFORM.match?(/mswin|mingw/) || RbConfig::CONFIG["YJIT_SUPPORT"] != "yes" ||
|
|
10
|
+
(defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?) || ENV["CANOPUS_YJIT_REEXEC"] == "1"
|
|
11
|
+
ENV["CANOPUS_YJIT_REEXEC"] = "1"
|
|
12
|
+
flags = ["--yjit"]
|
|
13
|
+
exec(RbConfig.ruby, *flags, __FILE__, *ARGV)
|
|
14
|
+
end
|
|
15
|
+
ENV.delete("CANOPUS_YJIT_REEXEC")
|
|
16
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
17
|
+
require "canopus"
|
|
18
|
+
require "canopus/cli"
|
|
19
|
+
exit Canopus::CLI.main(ARGV)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
class BlockMap
|
|
5
|
+
Block = Data.define(:id, :row, :text, :kind)
|
|
6
|
+
attr_reader :blocks
|
|
7
|
+
def initialize = @blocks = {}
|
|
8
|
+
def insert(id, row:, text:, kind: :diagnostic) = @blocks[id] = Block.new(id, row, text, kind)
|
|
9
|
+
def remove(id) = @blocks.delete(id)
|
|
10
|
+
def at(row) = @blocks.values.select { |block| block.row == row }
|
|
11
|
+
def apply(patch)
|
|
12
|
+
@blocks.transform_values! do |block|
|
|
13
|
+
old = patch.before.line_start([block.row, patch.before.line_count - 1].min)
|
|
14
|
+
row = patch.after.point_at(patch.map_offset(old, bias: :left)).row
|
|
15
|
+
Block.new(block.id, row, block.text, block.kind)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|