ruby-test-ide 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/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/exe/ruby-test-ide +42 -0
- data/lib/ruby_test_ide/observer.rb +97 -0
- data/lib/ruby_test_ide/public/index.html +611 -0
- data/lib/ruby_test_ide/public/vendor/monaco/LICENSE +21 -0
- data/lib/ruby_test_ide/public/vendor/monaco/ThirdPartyNotices.txt +448 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.de.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.es.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.fr.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.it.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ja.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ko.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ru.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-tw.js +8 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/base/worker/workerMain.js +27 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.css +6 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.js +745 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.de.js +31 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.es.js +31 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.fr.js +29 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.it.js +29 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ja.js +31 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.js +29 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ko.js +29 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ru.js +31 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +31 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.zh-tw.js +29 -0
- data/lib/ruby_test_ide/public/vendor/monaco/vs/loader.js +11 -0
- data/lib/ruby_test_ide/runner.rb +532 -0
- data/lib/ruby_test_ide/server.rb +724 -0
- data/lib/ruby_test_ide/version.rb +5 -0
- data/lib/ruby_test_ide.rb +12 -0
- metadata +166 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8573fe0b8988789da6eabe7b6baf637a1202836f754c3a42a2bd96382bccd9b5
|
|
4
|
+
data.tar.gz: 53184e3a3e26f9d2f68380bca1f7c4bbc61f0abdf0e7d847c541df3e519c22be
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e0892bf3f0e8357194dc3642aae916be6b758ae8ef9a568a1cf2850481cc97beabc5963cdc8603696c838877dfbb41c6bc9e0424b8d92f8e1253259a7ea660c0
|
|
7
|
+
data.tar.gz: a20ca8a9aad2c16cb30d9be04c0efde597d8199c32cfeb81ecde8e5278355069c5991b80874eeab2912f94ae1486f54ed0993d20583bd9635ccd89ba11569403
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samuel Garratt
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# ruby-test-ide
|
|
2
|
+
|
|
3
|
+
A browser IDE for Ruby whose intelligence comes from **running your code and
|
|
4
|
+
asking the live objects what they can do** — the same trick `irb` uses —
|
|
5
|
+
plus argument/return types **observed while your unit tests run**, instead of
|
|
6
|
+
static analysis or type-annotation files (sorbet/RBS).
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
t = 'test' # evaluated for real, in a sandboxed worker process
|
|
10
|
+
t.st # completion asks the actual String instance:
|
|
11
|
+
# -> start_with? strip strip! (with arity + owner)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Run it
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install ruby-test-ide
|
|
18
|
+
cd your/ruby/project
|
|
19
|
+
ruby-test-ide # prints e.g. http://localhost:3000/?token=<random> — open that
|
|
20
|
+
ruby-test-ide -p 4000 -w path/to/project # or pick port/workspace
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The server executes arbitrary code from any request it accepts, so a random
|
|
24
|
+
access token is required by default — copy the full URL it prints (including
|
|
25
|
+
`?token=...`); the page then attaches that token to every API call itself,
|
|
26
|
+
so you won't need to touch the URL again for that session. `--no-auth`
|
|
27
|
+
disables the token for CI/automation; `--token yourvalue` pins a fixed one.
|
|
28
|
+
|
|
29
|
+
From this repo (development):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ruby exe/ruby-test-ide --workspace workspace # demo workspace (prints its own token)
|
|
33
|
+
rake test # unit tests (pure helpers, worker round-trip, auth gate)
|
|
34
|
+
rake e2e # browser suite (Playwright, isolated fixture workspace, --no-auth)
|
|
35
|
+
rake build # pkg/ruby-test-ide-x.y.z.gem
|
|
36
|
+
rake install # build + install locally
|
|
37
|
+
rake release # tag vX.Y.Z, push, publish to rubygems (maintainer only)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Zero runtime dependencies (TCPServer, RDoc/ri and TracePoint are stdlib);
|
|
41
|
+
ruby >= 3.2, verified on 3.2.2 and 3.4.9 — syntax diagnostics understand both
|
|
42
|
+
parser generations' error formats (3.4's Prism messages are richer and
|
|
43
|
+
surface as better squiggles).
|
|
44
|
+
|
|
45
|
+
The sidebar shows a collapsible folder tree (state persists across reloads)
|
|
46
|
+
of Ruby files (`*.rb`, `Gemfile`, `Rakefile` — not a general file browser, so
|
|
47
|
+
opening something never force-parses non-Ruby content as Ruby), is resizable
|
|
48
|
+
by dragging its right edge, opens files, `+` creates them, Cmd/Ctrl+S saves;
|
|
49
|
+
unsaved buffers keep their edits when you switch files and are marked with a
|
|
50
|
+
dot. All paths are fenced inside the workspace root (traversal rejected
|
|
51
|
+
server-side). An empty directory is seeded with a two-file example on first
|
|
52
|
+
boot.
|
|
53
|
+
|
|
54
|
+
What you get in the editor:
|
|
55
|
+
|
|
56
|
+
| Goal (CLAUDE.md) | How it works here |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Autocomplete of all methods + parameter counts | On `.`, the buffer up to the cursor is evaluated in a worker; the receiver expression is `eval`'d against that binding and its **live methods** are listed with `Method#parameters`-derived signatures, arity and owner class. Works for chains (`t.strip.rev`), literals (`[1,2].su`), hash elements (`person[:name].cap`), user-defined methods, locals, constants, keywords. |
|
|
59
|
+
| Ruby docs on hover | Hovering a method looks up the real `ri` database in-process (`RDoc::RI::Driver`, memoised, ~0.15 ms warm). Hovering a variable shows its class **and current value**. |
|
|
60
|
+
| Error highlighting | Syntax errors (compile-only check, never executed) and runtime errors (from live eval, with the failing line) become Monaco squiggles. |
|
|
61
|
+
| `irb`-grade experience | Better: with **Live eval** on, every edit re-runs the buffer under a `TracePoint`, so each line gets a grey debugger annotation of the locals it changed (`# t: String = "test"`), and the side panel shows the final state of every variable + captured stdout. |
|
|
62
|
+
| Go to definition | Cmd/Ctrl+click or F12 on any method jumps to its `Method#source_location` — the runtime answer, so it works for methods defined dynamically. Workspace files open in place; gem/core locations are shown in the status bar. |
|
|
63
|
+
| Multi-file workspace | Buffers evaluate *in place*: the worker chdirs to the file's directory and shims `require_relative`, so `main.rb` can require `lib/greeter.rb` and completion on a `Greeter` instance lists methods **defined in the other file** — signatures included, unsaved edits included. |
|
|
64
|
+
| Gems via Gemfile | If the workspace has a `Gemfile`, the worker activates it (`bundler/setup`) before evaluating, so `require 'rest-client'` resolves exactly as under `bundle exec` and completion/hover work on gem objects (`RestClient.` → `get(url, headers = …, &block)`). Run `bundle install` with the same ruby the server uses; a missing gem surfaces as a friendly error in the panel. |
|
|
65
|
+
| Types from any test framework or script | **Learn from tests** runs the commands from `.ruby_ide.yaml` (or auto-detected test files) with a type observer preloaded via `RUBYOPT` into every ruby process they start. Works with minitest, rspec, rake, cucumber, or a plain script — the IDE records, per method, the argument/return classes actually seen with sample values (`.ruby-ide/observations.json`, merged across commands and subprocesses). Completion falls back to this oracle when live eval can't produce the object — `w.current('x').` completes as `Hash` even though `current` sits on top of a mocked network call — and hover shows "Observed in unit tests: current(city: String) → Hash (e.g. {...}) — 3 calls". |
|
|
66
|
+
| Access control | A random token (Jupyter-style) is required on every route except `/health` and the vendored static assets, since this server executes whatever code it's sent. Checked via an `X-Ruby-Test-Ide-Token` header (attached automatically by the page once loaded) or a `?token=` query param on first load; comparison is constant-time. `--no-auth` / `--token` override it. |
|
|
67
|
+
|
|
68
|
+
`npx playwright test --project=chromium` runs the end-to-end suite
|
|
69
|
+
(autocomplete, live state, diagnostics) against a real browser; the config
|
|
70
|
+
boots its own server against an isolated fixture workspace automatically.
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
browser (lib/ruby_test_ide/public/index.html, Monaco vendored — no internet needed)
|
|
76
|
+
│ POST /api/complete /hover /diagnostics /run (JSON)
|
|
77
|
+
▼
|
|
78
|
+
lib/ruby_test_ide/server.rb — zero-gem TCPServer HTTP server
|
|
79
|
+
│ • splits "line before cursor" into receiver expression + prefix
|
|
80
|
+
│ (backward scanner: balanced brackets, strings, chains, ranges)
|
|
81
|
+
│ • syntax check via RubyVM::InstructionSequence.compile (never executes)
|
|
82
|
+
│ • ri doc lookup + memoised cache (RDoc::RI::Driver in-process,
|
|
83
|
+
│ `ri` CLI fallback)
|
|
84
|
+
│ spawns per request ──────────────┐
|
|
85
|
+
▼ │ stdin: one JSON request
|
|
86
|
+
lib/ruby_test_ide/runner.rb — worker │ fd 3: one JSON reply (user stdout
|
|
87
|
+
• fresh method-body binding │ can't corrupt the protocol)
|
|
88
|
+
(self == main, no leaked locals) │ pgroup + kill on 4s timeout
|
|
89
|
+
• eval(code_before, bind) — largest │ rlimit_cpu belt-and-braces
|
|
90
|
+
syntactically-valid prefix, keeps
|
|
91
|
+
partial state when code raises mid-way
|
|
92
|
+
• completion: eval(receiver).methods + Method#parameters
|
|
93
|
+
• run: TracePoint(:line) captures locals per line = debugger view
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Design decisions that came out of research into irb/repl_type_completor:
|
|
97
|
+
|
|
98
|
+
* **fd 3 for results, child process group, hard kill** — user code that
|
|
99
|
+
`puts`, forks, or loops forever cannot corrupt or hang the IDE.
|
|
100
|
+
* **Method-body binding as sandbox** — `TOPLEVEL_BINDING.eval('binding')`
|
|
101
|
+
leaks the host script's locals into completions; a `def ...; binding; end`
|
|
102
|
+
binding is clean and keeps script semantics (`self` is `main`).
|
|
103
|
+
* **`:line` TracePoint events fire *before* the line runs** — state shown
|
|
104
|
+
for line N is taken from the event for line N+1 (and from the binding
|
|
105
|
+
itself after eval returns, for the last line).
|
|
106
|
+
* **C-implemented core methods report `[[:rest]]`** (`gsub(*args)`) — real
|
|
107
|
+
parameter names for those come from the ri call-seq shown on hover.
|
|
108
|
+
(`Method#parameters` is fully informative for methods written in Ruby,
|
|
109
|
+
which includes gem code.)
|
|
110
|
+
* **`ENV['BUNDLER_VERSION']` is pinned before `bundler/setup`** — otherwise a
|
|
111
|
+
lockfile `BUNDLED WITH` an older installed bundler makes setup *re-exec*
|
|
112
|
+
the worker, whose stdin (the request) is already consumed.
|
|
113
|
+
* **Monaco's word pattern is overridden for Ruby** — the default splits
|
|
114
|
+
`start_with?` before the `?`, so hover/definition would query a method
|
|
115
|
+
that doesn't exist. Receiver evaluation for hover/completion also carries
|
|
116
|
+
its own 2s timeout, falling back to test observations, so hovering a chain
|
|
117
|
+
built on a slow network call stays responsive.
|
|
118
|
+
* **Monaco is vendored, not CDN-loaded** (`lib/ruby_test_ide/public/vendor/monaco`,
|
|
119
|
+
MIT-licensed, its own LICENSE ships alongside) — the IDE works fully offline
|
|
120
|
+
after `gem install`. Assets are served with a 1-year immutable cache, safe
|
|
121
|
+
only because `index.html` is templated at request time to cache-bust every
|
|
122
|
+
vendored URL with `?v=<gem version>` — an upgrade can't serve a stale
|
|
123
|
+
cached Monaco. A worker built from a `blob:` URL resolves `importScripts()`
|
|
124
|
+
against its own opaque origin, not the page's, so that URL must be
|
|
125
|
+
absolute (`window.location.origin`), not root-relative.
|
|
126
|
+
* **The token is only ever embedded into `index.html` after that request's
|
|
127
|
+
own token already checked out** — `/` requires `?token=...` like every
|
|
128
|
+
other route, so there's no window where an unauthenticated request could
|
|
129
|
+
see it. HTTP header names are case-insensitive but not hyphen-insensitive:
|
|
130
|
+
`X-RubyTestIDE-Token` downcases to `x-rubytestide-token` (no internal
|
|
131
|
+
hyphens), which silently failed against a hand-written
|
|
132
|
+
`x-ruby-test-ide-token` check — caught by a test that reads the real
|
|
133
|
+
header name out of `index.html` and drives it through `Server#route`,
|
|
134
|
+
rather than hand-copying the same literal on both sides.
|
|
135
|
+
|
|
136
|
+
## Docs prerequisite
|
|
137
|
+
|
|
138
|
+
Hover docs need an ri database. rvm rubies often ship without one; generate it
|
|
139
|
+
once (~20 s) with:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
cd ~/.rvm/src/ruby-3.2.2 && rdoc --all --ri \
|
|
143
|
+
--op ~/.rvm/rubies/ruby-3.2.2/share/ri/3.2.0/system .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Gems installed with the `--no-document` default have no ri data; add
|
|
147
|
+
`gem: --document=ri` to `~/.gemrc`, or backfill a gem with
|
|
148
|
+
`rdoc --ri --op "$(gem spec NAME doc_dir 2>/dev/null || echo $GEM_HOME/doc/NAME-VERSION)/ri" $GEM_HOME/gems/NAME-VERSION/lib`.
|
|
149
|
+
|
|
150
|
+
## Honest limitations (it's a PoC)
|
|
151
|
+
|
|
152
|
+
* **It executes your code.** That is the point — but treat it like running
|
|
153
|
+
`ruby file.rb`: file/network side effects happen (worker is killed after
|
|
154
|
+
4 s, though, so infinite loops are fine). A "Live eval" toggle turns
|
|
155
|
+
continuous execution off.
|
|
156
|
+
* Completion inside an unfinished `def`/block falls back to the largest
|
|
157
|
+
parseable prefix, so method-local variables aren't visible until the
|
|
158
|
+
block closes (irb has the same blind spot mid-edit).
|
|
159
|
+
* Each request pays worker spawn cost (~50 ms) plus a full re-run of the
|
|
160
|
+
buffer — fine for scripts, wrong for slow/side-effectful programs.
|
|
161
|
+
|
|
162
|
+
## How "learn from tests" works
|
|
163
|
+
|
|
164
|
+
Configure it in `workspace/.ruby_ide.yaml` — any executable counts, because
|
|
165
|
+
the observer doesn't run your tests, it *rides along* in them:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
learn:
|
|
169
|
+
- bundle exec rspec
|
|
170
|
+
- bundle exec rake test
|
|
171
|
+
- ruby scripts/exercise_everything.rb
|
|
172
|
+
timeout: 60 # seconds per command
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
POST /api/learn
|
|
177
|
+
server: read .ruby_ide.yaml learn: commands
|
|
178
|
+
(fallback: guess from test/**/*_test.rb and spec/)
|
|
179
|
+
each command runs from the workspace root with
|
|
180
|
+
RUBYOPT="-r .../observer.rb" RUBY_IDE_OBS_DIR RUBY_IDE_OBS_ROOT
|
|
181
|
+
observer (framework-agnostic, inherited by every ruby subprocess):
|
|
182
|
+
TracePoint(:call, :return) filtered to workspace paths
|
|
183
|
+
→ per method: {args: {name: {Class: sample}}, returns: {Class: sample}, calls}
|
|
184
|
+
→ dumped to one JSON file per process at exit
|
|
185
|
+
server: merge dumps → workspace/.ruby-ide/observations.json
|
|
186
|
+
completion/hover workers read the observations file:
|
|
187
|
+
- receiver eval fails → trailing method name → most-seen return class
|
|
188
|
+
→ complete on that class's instance_methods (flagged "observed in tests")
|
|
189
|
+
- hover merges live Method#parameters with observed types + sample values;
|
|
190
|
+
methods with no live object still get a full hover from observations
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Mocks are the mechanism, not a problem: tests stub the network edge
|
|
194
|
+
(`def @weather.fetch_json(_) = SAMPLE`), so everything *above* the stub runs
|
|
195
|
+
for real and the recorded types are real. The observer registers its
|
|
196
|
+
`at_exit` before minitest/rspec autorun hooks, so it dumps after the suite
|
|
197
|
+
finishes; paths like `<internal:hash>` are excluded so core methods don't
|
|
198
|
+
pollute the data. Note: `learn:` commands are arbitrary shell commands run
|
|
199
|
+
with your privileges — the same trust level as the IDE evaluating your code.
|
|
200
|
+
Ruby 3.4 note: minitest is a bundled gem, so it must be listed in the
|
|
201
|
+
workspace Gemfile to load under bundler.
|
|
202
|
+
|
|
203
|
+
## Where this goes next
|
|
204
|
+
|
|
205
|
+
1. **Persist + cache context** (as CLAUDE.md suggests): keep a warm worker
|
|
206
|
+
per buffer, snapshot line-states, invalidate only below the edited line.
|
|
207
|
+
2. **Wrap it as an LSP server** — the runner already produces everything
|
|
208
|
+
`textDocument/completion|hover|publishDiagnostics` need, which would make
|
|
209
|
+
the same engine drive VS Code natively as an extension.
|
|
210
|
+
3. **Hybrid inference for un-run code paths**: seed types from the live
|
|
211
|
+
binding, then abstract-interpret the current expression the way
|
|
212
|
+
`repl_type_completor` (irb's opt-in type completor) does — no execution
|
|
213
|
+
of the hovered chain, RBS-aware, still runtime-grounded.
|
|
214
|
+
4. **Deeper project awareness**: the file tree, per-file buffers and
|
|
215
|
+
cross-file requires exist now; next is rdoc generation for the user's own
|
|
216
|
+
lib/ (hover docs for their classes), file rename/delete, and evaluating
|
|
217
|
+
entry points other than the open buffer (e.g. run tests to learn types on
|
|
218
|
+
code paths the buffer doesn't reach).
|
data/exe/ruby-test-ide
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require_relative '../lib/ruby_test_ide/version'
|
|
7
|
+
|
|
8
|
+
options = { port: 3000, workspace: Dir.pwd, token: nil, no_auth: false }
|
|
9
|
+
OptionParser.new do |opts|
|
|
10
|
+
opts.banner = <<~BANNER
|
|
11
|
+
ruby-test-ide #{RubyTestIDE::VERSION} — runtime-reflection IDE for Ruby
|
|
12
|
+
|
|
13
|
+
Serves an IDE for a project directory. Code is EXECUTED in sandboxed
|
|
14
|
+
workers to power completion/hover, and "Learn from tests" observes your
|
|
15
|
+
test runs to know types on mocked/unreached paths. A random access token
|
|
16
|
+
is required by default (printed on boot) — this server will run
|
|
17
|
+
whatever it's asked to.
|
|
18
|
+
|
|
19
|
+
Usage: ruby-test-ide [options]
|
|
20
|
+
BANNER
|
|
21
|
+
opts.on('-p', '--port PORT', Integer, 'Port to listen on (default 3000)') { |v| options[:port] = v }
|
|
22
|
+
opts.on('-w', '--workspace DIR', 'Project directory to serve (default: current directory)') { |v| options[:workspace] = v }
|
|
23
|
+
opts.on('--token TOKEN', 'Use this token instead of generating a random one') { |v| options[:token] = v }
|
|
24
|
+
opts.on('--no-auth', 'Disable the access token (e.g. CI) — INSECURE: anyone reaching the port can run code as you') do
|
|
25
|
+
options[:no_auth] = true
|
|
26
|
+
end
|
|
27
|
+
opts.on('-v', '--version', 'Print version') do
|
|
28
|
+
puts RubyTestIDE::VERSION
|
|
29
|
+
exit
|
|
30
|
+
end
|
|
31
|
+
end.parse!
|
|
32
|
+
|
|
33
|
+
# Constants in the server derive from these — set before the require.
|
|
34
|
+
ENV['RUBY_TEST_IDE_WORKSPACE'] = File.expand_path(options[:workspace])
|
|
35
|
+
if options[:no_auth]
|
|
36
|
+
ENV['RUBY_TEST_IDE_NO_AUTH'] = '1'
|
|
37
|
+
else
|
|
38
|
+
ENV['RUBY_TEST_IDE_TOKEN'] = options[:token] || SecureRandom.hex(24)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
require_relative '../lib/ruby_test_ide'
|
|
42
|
+
RubyTestIDE.start(port: options[:port])
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RubyIDE type observer.
|
|
4
|
+
#
|
|
5
|
+
# Preloaded (via RUBYOPT="-r.../observer.rb") into every ruby process a
|
|
6
|
+
# "learn" command starts — rspec, minitest, rake, a plain script, and any
|
|
7
|
+
# ruby subprocess those spawn (RUBYOPT is inherited). Framework-agnostic:
|
|
8
|
+
# it doesn't run tests, it only watches methods defined under
|
|
9
|
+
# RUBY_IDE_OBS_ROOT while the host process does whatever it does, then dumps
|
|
10
|
+
# {method => {args, returns, samples, calls}} to a JSON file in
|
|
11
|
+
# RUBY_IDE_OBS_DIR at exit.
|
|
12
|
+
#
|
|
13
|
+
# Registered before the host's own code loads, so its at_exit runs AFTER
|
|
14
|
+
# minitest/rspec autorun hooks (at_exit is LIFO) — observations include the
|
|
15
|
+
# full test run. Every path is rescued: this file must never be the reason
|
|
16
|
+
# a user's test suite fails.
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
ruby_ide_root = ENV['RUBY_IDE_OBS_ROOT'].to_s
|
|
20
|
+
ruby_ide_dir = ENV['RUBY_IDE_OBS_DIR'].to_s
|
|
21
|
+
unless ruby_ide_root.empty? || ruby_ide_dir.empty?
|
|
22
|
+
require 'json'
|
|
23
|
+
|
|
24
|
+
ruby_ide_class_of = ->(obj) { ::Kernel.instance_method(:class).bind_call(obj).to_s }
|
|
25
|
+
ruby_ide_inspect = lambda do |val|
|
|
26
|
+
s = val.inspect
|
|
27
|
+
s.length > 140 ? "#{s[0, 139]}…" : s
|
|
28
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
29
|
+
'(uninspectable)'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
ruby_ide_observations = {}
|
|
33
|
+
# The entry script's path can be relative (`ruby test/foo_test.rb`);
|
|
34
|
+
# expand once per distinct path, not per event.
|
|
35
|
+
ruby_ide_path_cache = {}
|
|
36
|
+
ruby_ide_in_root = lambda do |path|
|
|
37
|
+
ruby_ide_path_cache.fetch(path) do
|
|
38
|
+
ruby_ide_path_cache[path] =
|
|
39
|
+
if path.start_with?('<', '(') # <internal:hash>, (eval), (irb)…
|
|
40
|
+
false
|
|
41
|
+
else
|
|
42
|
+
begin
|
|
43
|
+
File.expand_path(path).start_with?(ruby_ide_root)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
ruby_ide_tp = TracePoint.new(:call, :return) do |t|
|
|
52
|
+
path = t.path.to_s
|
|
53
|
+
next unless ruby_ide_in_root.call(path)
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
entry = ruby_ide_observations["#{t.defined_class}##{t.method_id}"] ||= {
|
|
57
|
+
'method' => t.method_id.to_s, 'owner' => t.defined_class.to_s,
|
|
58
|
+
'file' => path, 'line' => t.lineno, 'calls' => 0,
|
|
59
|
+
'args' => {}, 'returns' => {}
|
|
60
|
+
}
|
|
61
|
+
if t.event == :call
|
|
62
|
+
entry['calls'] += 1
|
|
63
|
+
b = t.binding
|
|
64
|
+
t.parameters.each do |_type, pname|
|
|
65
|
+
next unless pname.is_a?(Symbol)
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
val = b.local_variable_get(pname)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
slot = entry['args'][pname.to_s] ||= {}
|
|
73
|
+
slot[ruby_ide_class_of.call(val)] ||= ruby_ide_inspect.call(val) if slot.size < 4
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
slot = entry['returns']
|
|
77
|
+
slot[ruby_ide_class_of.call(t.return_value)] ||= ruby_ide_inspect.call(t.return_value) if slot.size < 4
|
|
78
|
+
end
|
|
79
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
80
|
+
# observation must never break the host process
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
ruby_ide_tp.enable
|
|
84
|
+
|
|
85
|
+
at_exit do
|
|
86
|
+
ruby_ide_tp.disable
|
|
87
|
+
File.write(
|
|
88
|
+
File.join(ruby_ide_dir, "obs-#{Process.pid}-#{Time.now.to_f.to_s.tr('.', '')}.json"),
|
|
89
|
+
JSON.generate(ruby_ide_observations.first(2000).to_h)
|
|
90
|
+
)
|
|
91
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
96
|
+
nil
|
|
97
|
+
end
|