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.
Files changed (146) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +148 -0
  5. data/docs/adr/001-runtime-gem-components.md +24 -0
  6. data/docs/adr/002-bounded-diff-engine.md +22 -0
  7. data/docs/adr/003-background-language-analysis.md +22 -0
  8. data/docs/adr/004-persistent-display-map.md +23 -0
  9. data/docs/adr/005-windows-runtime.md +27 -0
  10. data/docs/adr/README.md +36 -0
  11. data/docs/distribution.md +27 -0
  12. data/docs/lsp.md +38 -0
  13. data/docs/performance.md +40 -0
  14. data/docs/snippets.md +32 -0
  15. data/docs/vim.md +24 -0
  16. data/docs/workspace_edits.md +32 -0
  17. data/examples/native_smoke.rb +25 -0
  18. data/examples/plugins/word_count.rb +11 -0
  19. data/exe/canopus +19 -0
  20. data/lib/canopus/block_map.rb +19 -0
  21. data/lib/canopus/buffer.rb +293 -0
  22. data/lib/canopus/cli.rb +171 -0
  23. data/lib/canopus/controller.rb +507 -0
  24. data/lib/canopus/data_compat.rb +25 -0
  25. data/lib/canopus/display_map/line_builder.rb +54 -0
  26. data/lib/canopus/display_map/line_set.rb +8 -0
  27. data/lib/canopus/display_map/pending_line_set.rb +9 -0
  28. data/lib/canopus/display_map/summary.rb +23 -0
  29. data/lib/canopus/display_map/worker.rb +89 -0
  30. data/lib/canopus/display_map.rb +350 -0
  31. data/lib/canopus/display_point.rb +5 -0
  32. data/lib/canopus/editor/snippet_expandable.rb +281 -0
  33. data/lib/canopus/editor.rb +445 -0
  34. data/lib/canopus/error.rb +5 -0
  35. data/lib/canopus/fold_map.rb +87 -0
  36. data/lib/canopus/git/blame.rb +50 -0
  37. data/lib/canopus/git/commit.rb +7 -0
  38. data/lib/canopus/git/corrupt_object.rb +7 -0
  39. data/lib/canopus/git/diff.rb +131 -0
  40. data/lib/canopus/git/index.rb +94 -0
  41. data/lib/canopus/git/object_database.rb +45 -0
  42. data/lib/canopus/git/pack.rb +186 -0
  43. data/lib/canopus/git/repository.rb +313 -0
  44. data/lib/canopus/git/status.rb +74 -0
  45. data/lib/canopus/git/tree_entry.rb +7 -0
  46. data/lib/canopus/git.rb +15 -0
  47. data/lib/canopus/icon_theme.rb +43 -0
  48. data/lib/canopus/language/background_analysis/job.rb +12 -0
  49. data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
  50. data/lib/canopus/language/background_analysis.rb +280 -0
  51. data/lib/canopus/language/definition.rb +7 -0
  52. data/lib/canopus/language/document.rb +143 -0
  53. data/lib/canopus/language/symbol.rb +7 -0
  54. data/lib/canopus/language/syntax_worker.rb +91 -0
  55. data/lib/canopus/language.rb +42 -0
  56. data/lib/canopus/lazy_rope.rb +218 -0
  57. data/lib/canopus/lsp/client.rb +299 -0
  58. data/lib/canopus/lsp/error.rb +7 -0
  59. data/lib/canopus/lsp/future/subscription.rb +9 -0
  60. data/lib/canopus/lsp/future.rb +87 -0
  61. data/lib/canopus/lsp/protocol.rb +83 -0
  62. data/lib/canopus/lsp/server_error.rb +13 -0
  63. data/lib/canopus/lsp/timeout.rb +7 -0
  64. data/lib/canopus/lsp/transport.rb +123 -0
  65. data/lib/canopus/lsp.rb +19 -0
  66. data/lib/canopus/markdown.rb +69 -0
  67. data/lib/canopus/match_data_compat.rb +17 -0
  68. data/lib/canopus/multi_buffer.rb +266 -0
  69. data/lib/canopus/pane.rb +61 -0
  70. data/lib/canopus/patch/composite.rb +12 -0
  71. data/lib/canopus/patch/reload.rb +18 -0
  72. data/lib/canopus/patch.rb +27 -0
  73. data/lib/canopus/performance_recorder.rb +207 -0
  74. data/lib/canopus/plugins/api.rb +50 -0
  75. data/lib/canopus/plugins/isolated_runtime.rb +167 -0
  76. data/lib/canopus/plugins/local_runtime.rb +25 -0
  77. data/lib/canopus/plugins/permission_denied.rb +7 -0
  78. data/lib/canopus/plugins/registry.rb +30 -0
  79. data/lib/canopus/plugins.rb +11 -0
  80. data/lib/canopus/project/ignore_matcher.rb +104 -0
  81. data/lib/canopus/project/search.rb +164 -0
  82. data/lib/canopus/project/search_worker/cancelled.rb +3 -0
  83. data/lib/canopus/project/search_worker/runner.rb +9 -0
  84. data/lib/canopus/project/search_worker.rb +108 -0
  85. data/lib/canopus/project/tree.rb +48 -0
  86. data/lib/canopus/project/watcher.rb +59 -0
  87. data/lib/canopus/project.rb +127 -0
  88. data/lib/canopus/regexp_compat.rb +30 -0
  89. data/lib/canopus/save_conflict.rb +5 -0
  90. data/lib/canopus/selection.rb +11 -0
  91. data/lib/canopus/settings.rb +118 -0
  92. data/lib/canopus/snippet/transform.rb +209 -0
  93. data/lib/canopus/snippet.rb +183 -0
  94. data/lib/canopus/tab_map.rb +30 -0
  95. data/lib/canopus/terminal/cell.rb +7 -0
  96. data/lib/canopus/terminal/grid.rb +321 -0
  97. data/lib/canopus/terminal/pty.rb +103 -0
  98. data/lib/canopus/terminal/scrollback.rb +38 -0
  99. data/lib/canopus/terminal/vt.rb +398 -0
  100. data/lib/canopus/terminal.rb +13 -0
  101. data/lib/canopus/theme.rb +43 -0
  102. data/lib/canopus/version.rb +5 -0
  103. data/lib/canopus/vim/commandable.rb +148 -0
  104. data/lib/canopus/vim/motionable.rb +321 -0
  105. data/lib/canopus/vim/operator_capable.rb +377 -0
  106. data/lib/canopus/vim/text_object_selectable.rb +141 -0
  107. data/lib/canopus/vim.rb +426 -0
  108. data/lib/canopus/workspace/edit/executable.rb +139 -0
  109. data/lib/canopus/workspace/edit/node.rb +8 -0
  110. data/lib/canopus/workspace/edit/plan.rb +170 -0
  111. data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
  112. data/lib/canopus/workspace/edit.rb +6 -0
  113. data/lib/canopus/workspace/file_change_aware.rb +40 -0
  114. data/lib/canopus/workspace/file_previewable.rb +31 -0
  115. data/lib/canopus/workspace/git_aware.rb +140 -0
  116. data/lib/canopus/workspace/language_aware.rb +413 -0
  117. data/lib/canopus/workspace/language_server_configurable.rb +181 -0
  118. data/lib/canopus/workspace/project_searchable.rb +208 -0
  119. data/lib/canopus/workspace/project_tree_editable.rb +82 -0
  120. data/lib/canopus/workspace/session_persistable.rb +153 -0
  121. data/lib/canopus/workspace/settings_aware.rb +91 -0
  122. data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
  123. data/lib/canopus/workspace/view.rb +643 -0
  124. data/lib/canopus/workspace.rb +525 -0
  125. data/lib/canopus/wrap_map.rb +108 -0
  126. data/lib/canopus.rb +24 -0
  127. data/sig/canopus.rbs +375 -0
  128. data/sig/controller.rbs +55 -0
  129. data/sig/display_stages.rbs +49 -0
  130. data/sig/git.rbs +140 -0
  131. data/sig/lsp.rbs +99 -0
  132. data/sig/markdown.rbs +11 -0
  133. data/sig/performance_recorder.rbs +23 -0
  134. data/sig/search_services.rbs +8 -0
  135. data/sig/services.rbs +81 -0
  136. data/sig/snippet.rbs +42 -0
  137. data/sig/terminal.rbs +125 -0
  138. data/sig/vim.rbs +27 -0
  139. data/sig/workspace_edits.rbs +11 -0
  140. data/sig/workspace_services.rbs +117 -0
  141. data/tools/certify_snippet_regex.rb +35 -0
  142. data/tools/check_dependencies.rb +80 -0
  143. data/tools/native_check.rb +70 -0
  144. data/tools/package.rb +113 -0
  145. data/tools/package_test.rb +32 -0
  146. 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-11
4
+
5
+ - Initial release.
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.
@@ -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
+ ```
@@ -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