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.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +218 -0
  4. data/exe/ruby-test-ide +42 -0
  5. data/lib/ruby_test_ide/observer.rb +97 -0
  6. data/lib/ruby_test_ide/public/index.html +611 -0
  7. data/lib/ruby_test_ide/public/vendor/monaco/LICENSE +21 -0
  8. data/lib/ruby_test_ide/public/vendor/monaco/ThirdPartyNotices.txt +448 -0
  9. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  10. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.de.js +8 -0
  11. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.es.js +8 -0
  12. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.fr.js +8 -0
  13. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.it.js +8 -0
  14. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ja.js +8 -0
  15. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +8 -0
  16. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ko.js +8 -0
  17. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.ru.js +8 -0
  18. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +8 -0
  19. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-tw.js +8 -0
  20. data/lib/ruby_test_ide/public/vendor/monaco/vs/base/worker/workerMain.js +27 -0
  21. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  22. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  23. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  24. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  25. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  26. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  27. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  28. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  29. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  30. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  31. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  32. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  33. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  34. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  35. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  36. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  37. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  38. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  39. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  40. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  41. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  42. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  43. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  44. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  45. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  46. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  47. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  48. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  49. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  50. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  51. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  52. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  53. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  54. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  55. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  56. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  57. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  58. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  59. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  60. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  61. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  62. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  63. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  64. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  65. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  66. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  67. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  68. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  69. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  70. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  71. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  72. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  73. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  74. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  75. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  76. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  77. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  78. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  79. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  80. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  81. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  82. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  83. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  84. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  85. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  86. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  87. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  88. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  89. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  90. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  91. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  92. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  93. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  94. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  95. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  96. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  97. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  98. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  99. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  100. data/lib/ruby_test_ide/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  101. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.css +6 -0
  102. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.js +745 -0
  103. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.de.js +31 -0
  104. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.es.js +31 -0
  105. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.fr.js +29 -0
  106. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.it.js +29 -0
  107. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ja.js +31 -0
  108. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.js +29 -0
  109. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ko.js +29 -0
  110. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.ru.js +31 -0
  111. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +31 -0
  112. data/lib/ruby_test_ide/public/vendor/monaco/vs/editor/editor.main.nls.zh-tw.js +29 -0
  113. data/lib/ruby_test_ide/public/vendor/monaco/vs/loader.js +11 -0
  114. data/lib/ruby_test_ide/runner.rb +532 -0
  115. data/lib/ruby_test_ide/server.rb +724 -0
  116. data/lib/ruby_test_ide/version.rb +5 -0
  117. data/lib/ruby_test_ide.rb +12 -0
  118. metadata +166 -0
@@ -0,0 +1,611 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Ruby IDE — runtime reflection PoC</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ :root {
9
+ --bg: #1e1e1e; --panel: #252526; --border: #333; --fg: #d4d4d4;
10
+ --dim: #858585; --accent: #4ec9b0; --err: #f48771;
11
+ }
12
+ * { box-sizing: border-box; }
13
+ html, body { height: 100%; margin: 0; }
14
+ body {
15
+ display: flex; flex-direction: column; background: var(--bg); color: var(--fg);
16
+ font: 13px/1.5 -apple-system, "Segoe UI", sans-serif;
17
+ }
18
+ header {
19
+ display: flex; align-items: center; gap: 12px; padding: 8px 14px;
20
+ background: var(--panel); border-bottom: 1px solid var(--border);
21
+ }
22
+ header h1 { font-size: 14px; margin: 0; font-weight: 600; }
23
+ header h1 span { color: var(--accent); }
24
+ #current-file { font: 12px ui-monospace, Menlo, monospace; color: var(--dim); }
25
+ #current-file.dirty::after { content: ' ●'; color: #e2c08d; }
26
+ header .spacer { flex: 1; }
27
+ button, label.toggle {
28
+ background: #0e639c; border: none; color: #fff; padding: 5px 14px;
29
+ border-radius: 3px; font-size: 12px; cursor: pointer;
30
+ }
31
+ button:hover { background: #1177bb; }
32
+ button.ghost { background: #3a3d41; }
33
+ button.ghost:hover { background: #45484d; }
34
+ label.toggle { background: transparent; color: var(--fg); display: flex; align-items: center; gap: 6px; }
35
+ #status { font-size: 12px; color: var(--dim); min-width: 150px; text-align: right; }
36
+ main { display: flex; flex: 1; min-height: 0; }
37
+ nav {
38
+ width: 200px; min-width: 120px; max-width: 70vw; flex: none;
39
+ background: var(--panel); display: flex; flex-direction: column; overflow: hidden;
40
+ }
41
+ #nav-resizer {
42
+ width: 4px; flex: none; cursor: col-resize; background: var(--border);
43
+ }
44
+ #nav-resizer:hover, #nav-resizer.dragging { background: #007acc; }
45
+ nav .nav-head {
46
+ display: flex; align-items: center; justify-content: space-between;
47
+ padding: 10px 12px 4px;
48
+ }
49
+ nav h2 {
50
+ font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
51
+ color: var(--dim); margin: 0;
52
+ }
53
+ #new-file {
54
+ background: transparent; color: var(--dim); padding: 0 4px; font-size: 16px; line-height: 1;
55
+ }
56
+ #new-file:hover { color: var(--fg); background: transparent; }
57
+ #files { overflow: auto; flex: 1; padding: 4px 0; }
58
+ #files .file, #files .dir {
59
+ padding: 3px 10px; cursor: pointer; font: 12px ui-monospace, Menlo, monospace;
60
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg);
61
+ }
62
+ #files .file:hover, #files .dir:hover { background: #2a2d2e; }
63
+ #files .file.active { background: #37373d; color: #fff; }
64
+ #files .file.dirty::after { content: ' ●'; color: #e2c08d; }
65
+ #files .dir { color: var(--dim); user-select: none; }
66
+ #files .dir .chevron { display: inline-block; width: 1em; }
67
+ #editor { flex: 1; min-width: 0; }
68
+ aside {
69
+ width: 330px; background: var(--panel); border-left: 1px solid var(--border);
70
+ display: flex; flex-direction: column; overflow-y: auto;
71
+ }
72
+ aside h2 {
73
+ font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
74
+ color: var(--dim); margin: 0; padding: 10px 12px 4px;
75
+ }
76
+ aside section { border-bottom: 1px solid var(--border); padding-bottom: 8px; }
77
+ table { width: 100%; border-collapse: collapse; font: 12px ui-monospace, Menlo, monospace; }
78
+ td { padding: 2px 12px; vertical-align: top; }
79
+ td.var { color: #9cdcfe; white-space: nowrap; }
80
+ td.klass { color: var(--accent); white-space: nowrap; }
81
+ td.val { color: #ce9178; word-break: break-all; }
82
+ pre {
83
+ margin: 4px 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;
84
+ font: 12px ui-monospace, Menlo, monospace; white-space: pre-wrap; word-break: break-word;
85
+ }
86
+ #run-error { color: var(--err); }
87
+ .empty { color: var(--dim); padding: 2px 12px; font-style: italic; }
88
+ .inline-state { color: #6a7a6a !important; font-style: italic; opacity: .9; }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <header>
93
+ <h1>Ruby<span>IDE</span></h1>
94
+ <div id="current-file"></div>
95
+ <div class="spacer"></div>
96
+ <label class="toggle"><input type="checkbox" id="live" checked> Live eval</label>
97
+ <button id="save" class="ghost" title="Cmd/Ctrl+S">Save</button>
98
+ <button id="learn" class="ghost" title="Run the workspace tests and learn argument/return types from what they execute">Learn from tests</button>
99
+ <button id="run">Run &#9654;</button>
100
+ <div id="status">starting&hellip;</div>
101
+ </header>
102
+ <main>
103
+ <nav>
104
+ <div class="nav-head">
105
+ <h2>Workspace</h2>
106
+ <button id="new-file" title="New file">+</button>
107
+ </div>
108
+ <div id="files"></div>
109
+ </nav>
110
+ <div id="nav-resizer" title="Drag to resize"></div>
111
+ <div id="editor"></div>
112
+ <aside>
113
+ <section>
114
+ <h2>Runtime state</h2>
115
+ <table id="state"></table>
116
+ <div class="empty" id="state-empty">run the buffer to inspect live objects</div>
117
+ </section>
118
+ <section>
119
+ <h2>Output</h2>
120
+ <pre id="stdout"></pre>
121
+ </section>
122
+ <section>
123
+ <h2>Runtime error</h2>
124
+ <pre id="run-error"></pre>
125
+ </section>
126
+ <section>
127
+ <h2>Tests</h2>
128
+ <div class="empty" id="tests-empty">"Learn from tests" runs the suite and records real argument/return types</div>
129
+ <pre id="tests-out" style="display:none"></pre>
130
+ </section>
131
+ </aside>
132
+ </main>
133
+
134
+ <!-- Monaco is vendored (lib/ruby_test_ide/public/vendor/monaco) so the IDE
135
+ works with no internet access beyond the initial `gem install`. The
136
+ ?v= query cache-busts across gem upgrades (server.rb templates it in;
137
+ serve_static caches these files for a year since the URL is versioned). -->
138
+ <script src="/vendor/monaco/vs/loader.js?v=__RTI_VERSION__"></script>
139
+ <script>
140
+ // Absolute (not root-relative): a worker built from a blob: URL resolves
141
+ // importScripts() against the blob's own opaque origin, not the page's.
142
+ const MONACO_BASE = new URL('/vendor/monaco', window.location.origin).href;
143
+ const MONACO_QS = '?v=__RTI_VERSION__';
144
+ require.config({ paths: { vs: MONACO_BASE + '/vs' }, urlArgs: 'v=__RTI_VERSION__' });
145
+ window.MonacoEnvironment = {
146
+ getWorkerUrl: () => URL.createObjectURL(new Blob([
147
+ `self.MonacoEnvironment={baseUrl:'${MONACO_BASE}/'};` +
148
+ `importScripts('${MONACO_BASE}/vs/base/worker/workerMain.js${MONACO_QS}');`
149
+ ], { type: 'text/javascript' }))
150
+ };
151
+
152
+ // The server verified this page's ?token=... before serving it, and
153
+ // embedded that same token here so every subsequent API call can carry it
154
+ // as a header — the browser never needs to re-read it from the URL.
155
+ const RTI_TOKEN = '__RTI_TOKEN__';
156
+ const AUTH_HEADERS = RTI_TOKEN ? { 'X-Ruby-Test-Ide-Token': RTI_TOKEN } : {};
157
+
158
+ const $ = id => document.getElementById(id);
159
+ const post = async (path, body) => {
160
+ const res = await fetch(path, { method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify(body) });
161
+ return res.json();
162
+ };
163
+ const get = async (path) => (await fetch(path, { headers: AUTH_HEADERS })).json();
164
+ const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
165
+ const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;');
166
+
167
+ require(['vs/editor/editor.main'], () => {
168
+ const editor = monaco.editor.create($('editor'), {
169
+ language: 'ruby',
170
+ theme: 'vs-dark',
171
+ ariaLabel: 'Document',
172
+ fontSize: 13,
173
+ minimap: { enabled: false },
174
+ automaticLayout: true,
175
+ suggest: { showWords: false }, // only OUR runtime results, no text-buffer noise
176
+ quickSuggestions: { other: true, comments: false, strings: false },
177
+ fixedOverflowWidgets: true,
178
+ });
179
+ window.__editor = editor; // handy for tests / console poking
180
+
181
+ // Ruby words may end in ? or ! (start_with?, save!) and start with @/$ —
182
+ // Monaco's default pattern would hand the server "start_with" and the
183
+ // lookup would miss. (Replaces the stock language config; comments and
184
+ // brackets are re-declared so nothing is lost.)
185
+ monaco.languages.setLanguageConfiguration('ruby', {
186
+ comments: { lineComment: '#', blockComment: ['=begin', '=end'] },
187
+ brackets: [['{', '}'], ['[', ']'], ['(', ')']],
188
+ autoClosingPairs: [
189
+ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' },
190
+ { open: '"', close: '"' }, { open: "'", close: "'" },
191
+ ],
192
+ wordPattern: /(@@?|\$)?[A-Za-z_][A-Za-z0-9_]*[?!]?/g,
193
+ });
194
+
195
+ // Explicit user actions (save, create) set a sticky status that background
196
+ // updates (live eval chatter) may not overwrite for a few seconds.
197
+ let statusLockedUntil = 0;
198
+ const setStatus = (t, { sticky = false } = {}) => {
199
+ const now = Date.now();
200
+ if (!sticky && now < statusLockedUntil) return;
201
+ if (sticky) statusLockedUntil = now + 3000;
202
+ $('status').textContent = t;
203
+ };
204
+
205
+ // -------------------------------------------------------------- workspace
206
+ // One Monaco model per file; unsaved edits survive switching files.
207
+ const buffers = new Map(); // path -> { model, savedVersion }
208
+ let currentPath = null;
209
+ let fileList = [];
210
+
211
+ const isDirty = path => {
212
+ const b = buffers.get(path);
213
+ return b && b.model.getAlternativeVersionId() !== b.savedVersion;
214
+ };
215
+
216
+ // Directory collapse state persists across reloads; a dir is expanded
217
+ // unless its full path (e.g. "lib/models") is in this set.
218
+ const collapsedDirs = new Set(JSON.parse(localStorage.getItem('rti:collapsedDirs') || '[]'));
219
+ const persistCollapsedDirs = () =>
220
+ localStorage.setItem('rti:collapsedDirs', JSON.stringify([...collapsedDirs]));
221
+
222
+ // fileList (flat, "lib/greeter.rb") -> { dirs: Map<name, node>, files: [...] }
223
+ const buildFileTree = (files) => {
224
+ const root = { dirs: new Map(), files: [] };
225
+ for (const f of files) {
226
+ const parts = f.path.split('/');
227
+ let node = root;
228
+ let prefix = '';
229
+ for (let i = 0; i < parts.length - 1; i++) {
230
+ prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
231
+ if (!node.dirs.has(parts[i])) node.dirs.set(parts[i], { dirs: new Map(), files: [], fullPath: prefix });
232
+ node = node.dirs.get(parts[i]);
233
+ }
234
+ node.files.push(f);
235
+ }
236
+ return root;
237
+ };
238
+
239
+ const renderTreeNode = (node, depth, container) => {
240
+ const indent = 10 + depth * 14;
241
+ for (const name of [...node.dirs.keys()].sort((a, b) => a.localeCompare(b))) {
242
+ const child = node.dirs.get(name);
243
+ const collapsed = collapsedDirs.has(child.fullPath);
244
+ const row = document.createElement('div');
245
+ row.className = 'dir';
246
+ row.style.paddingLeft = indent + 'px';
247
+ row.innerHTML = `<span class="chevron">${collapsed ? '▸' : '▾'}</span>${esc(name)}`;
248
+ row.title = child.fullPath;
249
+ row.addEventListener('click', () => {
250
+ collapsedDirs.has(child.fullPath) ? collapsedDirs.delete(child.fullPath) : collapsedDirs.add(child.fullPath);
251
+ persistCollapsedDirs();
252
+ renderFileList();
253
+ });
254
+ container.appendChild(row);
255
+ if (!collapsed) renderTreeNode(child, depth + 1, container);
256
+ }
257
+ for (const f of [...node.files].sort((a, b) => a.path.localeCompare(b.path))) {
258
+ const el = document.createElement('div');
259
+ el.className = 'file' + (f.path === currentPath ? ' active' : '') +
260
+ (isDirty(f.path) ? ' dirty' : '');
261
+ el.style.paddingLeft = (indent + 14) + 'px';
262
+ el.textContent = f.path.split('/').pop();
263
+ el.title = f.path;
264
+ el.addEventListener('click', () => openFile(f.path));
265
+ container.appendChild(el);
266
+ }
267
+ };
268
+
269
+ const renderFileList = () => {
270
+ $('files').innerHTML = '';
271
+ renderTreeNode(buildFileTree(fileList), 0, $('files'));
272
+ const cf = $('current-file');
273
+ cf.textContent = currentPath || '';
274
+ cf.classList.toggle('dirty', !!(currentPath && isDirty(currentPath)));
275
+ };
276
+
277
+ // ------------------------------------------------------ resizable sidebar
278
+ (() => {
279
+ const nav = document.querySelector('nav');
280
+ const resizer = $('nav-resizer');
281
+ const saved = parseInt(localStorage.getItem('rti:navWidth'), 10);
282
+ if (saved) nav.style.width = saved + 'px';
283
+
284
+ let startX = 0, startWidth = 0;
285
+ const onMove = (e) => {
286
+ const dx = e.clientX - startX;
287
+ const min = parseInt(getComputedStyle(nav).minWidth, 10);
288
+ const max = window.innerWidth * 0.7;
289
+ nav.style.width = Math.max(min, Math.min(max, startWidth + dx)) + 'px';
290
+ };
291
+ const onUp = () => {
292
+ resizer.classList.remove('dragging');
293
+ document.removeEventListener('mousemove', onMove);
294
+ document.removeEventListener('mouseup', onUp);
295
+ localStorage.setItem('rti:navWidth', parseInt(nav.style.width, 10));
296
+ if (window.__editor) window.__editor.layout();
297
+ };
298
+ resizer.addEventListener('mousedown', (e) => {
299
+ startX = e.clientX;
300
+ startWidth = nav.getBoundingClientRect().width;
301
+ resizer.classList.add('dragging');
302
+ document.addEventListener('mousemove', onMove);
303
+ document.addEventListener('mouseup', onUp);
304
+ e.preventDefault();
305
+ });
306
+ })();
307
+
308
+ const refreshFiles = async () => {
309
+ fileList = (await get('/api/files')).files || [];
310
+ renderFileList();
311
+ };
312
+
313
+ const ensureBuffer = async (path) => {
314
+ if (!buffers.has(path)) {
315
+ const data = await get('/api/file?path=' + encodeURIComponent(path));
316
+ const model = monaco.editor.createModel(data.content, 'ruby',
317
+ monaco.Uri.from({ scheme: 'ruby-ide', path: '/' + path }));
318
+ buffers.set(path, { model, savedVersion: model.getAlternativeVersionId() });
319
+ }
320
+ return buffers.get(path);
321
+ };
322
+
323
+ const openFile = async (path) => {
324
+ await ensureBuffer(path);
325
+ currentPath = path;
326
+ editor.setModel(buffers.get(path).model);
327
+ decorations = []; // decoration ids belong to the old model
328
+ runtimeMarker = [];
329
+ renderFileList();
330
+ editor.focus();
331
+ refreshDiagnostics();
332
+ if ($('live').checked) runBuffer();
333
+ };
334
+
335
+ const saveFile = async () => {
336
+ if (!currentPath) return;
337
+ const b = buffers.get(currentPath);
338
+ const res = await post('/api/file', { path: currentPath, content: b.model.getValue() });
339
+ if (res.ok) {
340
+ b.savedVersion = b.model.getAlternativeVersionId();
341
+ setStatus('saved ' + res.path, { sticky: true });
342
+ await refreshFiles();
343
+ } else {
344
+ setStatus('save failed: ' + (res.error || 'unknown'), { sticky: true });
345
+ }
346
+ };
347
+
348
+ const newFile = async () => {
349
+ const name = prompt('New file path (relative to workspace):', 'untitled.rb');
350
+ if (!name) return;
351
+ const res = await post('/api/file', { path: name, content: '' });
352
+ if (res.error) { setStatus('create failed: ' + res.error); return; }
353
+ await refreshFiles();
354
+ openFile(name);
355
+ };
356
+
357
+ $('save').addEventListener('click', saveFile);
358
+ $('new-file').addEventListener('click', newFile);
359
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveFile);
360
+ window.addEventListener('keydown', e => {
361
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); saveFile(); }
362
+ });
363
+
364
+ const KINDS = {
365
+ method: monaco.languages.CompletionItemKind.Method,
366
+ variable: monaco.languages.CompletionItemKind.Variable,
367
+ constant: monaco.languages.CompletionItemKind.Class,
368
+ keyword: monaco.languages.CompletionItemKind.Keyword,
369
+ };
370
+
371
+ // ---------------------------------------------------------- autocomplete
372
+ monaco.languages.registerCompletionItemProvider('ruby', {
373
+ triggerCharacters: ['.'],
374
+ provideCompletionItems: async (mdl, position) => {
375
+ const res = await post('/api/complete', {
376
+ code: mdl.getValue(), line: position.lineNumber, column: position.column,
377
+ path: currentPath,
378
+ });
379
+ setStatus(res.receiver
380
+ ? `${res.receiver} : ${res.receiver_class || '?'}${res.oracle ? ' (from tests)' : ''}`
381
+ : 'context evaluated');
382
+ const word = mdl.getWordUntilPosition(position);
383
+ const range = new monaco.Range(
384
+ position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
385
+ return {
386
+ suggestions: (res.items || []).map(it => ({
387
+ label: it.name,
388
+ kind: KINDS[it.kind] || KINDS.method,
389
+ detail: it.kind === 'variable'
390
+ ? `${it.class || ''} ${it.value !== undefined ? '= ' + it.value : ''}`
391
+ : [it.signature, it.owner && `— ${it.owner}`].filter(Boolean).join(' '),
392
+ documentation: it.arity !== undefined
393
+ ? { value: `arity: \`${it.arity}\`` } : undefined,
394
+ insertText: it.name,
395
+ sortText: String(it.sort ?? 50).padStart(3, '0') + it.name,
396
+ range,
397
+ })),
398
+ };
399
+ },
400
+ });
401
+
402
+ // ----------------------------------------------------------------- hover
403
+ monaco.languages.registerHoverProvider('ruby', {
404
+ provideHover: async (mdl, position) => {
405
+ const word = mdl.getWordAtPosition(position);
406
+ if (!word) return null;
407
+ const res = await post('/api/hover', {
408
+ code: mdl.getValue(), line: position.lineNumber, column: position.column,
409
+ word: word.word, wordStartColumn: word.startColumn,
410
+ path: currentPath,
411
+ });
412
+ if (!res || !res.kind) return null;
413
+ const md = [];
414
+ if (res.kind === 'method') {
415
+ md.push({ value: '```ruby\n' + `${res.receiver_class}#${res.signature}` + '\n```' });
416
+ md.push({ value: `defined in **${res.owner}**` +
417
+ (res.arity !== undefined ? ` · arity \`${res.arity}\`` : '') +
418
+ (res.source ? `\n\nsource: \`${res.source}\`` : '') });
419
+ } else {
420
+ md.push({ value: '```ruby\n' + `${res.name} : ${res.class}` +
421
+ (res.value !== undefined ? ` = ${res.value}` : '') + '\n```' });
422
+ if (res.ancestors) md.push({ value: 'ancestors: ' + res.ancestors.join(' → ') });
423
+ }
424
+ if (res.via) md.push({ value: `_receiver type via ${res.via}_` });
425
+ if (res.observed && res.observed.length) {
426
+ const lines = res.observed.map(o => {
427
+ const args = Object.entries(o.args || {})
428
+ .map(([n, classes]) => `${n}: ${Object.keys(classes).join(' | ')}`).join(', ');
429
+ const rets = Object.entries(o.returns || {})
430
+ .map(([cls, sample]) => `${cls} (e.g. ${sample})`).join(' | ') || '?';
431
+ return `\`${o.method}(${args}) → \` ${rets} — ${o.calls} call${o.calls === 1 ? '' : 's'}`;
432
+ });
433
+ md.push({ value: '**Observed in unit tests:**\n\n' + lines.join('\n\n') });
434
+ }
435
+ if (res.doc) md.push({ value: res.doc.slice(0, 1500) });
436
+ return {
437
+ range: new monaco.Range(position.lineNumber, word.startColumn,
438
+ position.lineNumber, word.endColumn),
439
+ contents: md,
440
+ };
441
+ },
442
+ });
443
+
444
+ // ------------------------------------------------------ go to definition
445
+ // The runner already knows Method#source_location — Cmd/Ctrl+click or F12
446
+ // jumps to it when it's a workspace file.
447
+ monaco.languages.registerDefinitionProvider('ruby', {
448
+ provideDefinition: async (mdl, position) => {
449
+ const word = mdl.getWordAtPosition(position);
450
+ if (!word) return null;
451
+ const res = await post('/api/hover', {
452
+ code: mdl.getValue(), line: position.lineNumber, column: position.column,
453
+ word: word.word, wordStartColumn: word.startColumn,
454
+ path: currentPath,
455
+ });
456
+ if (!res || !res.source) return null;
457
+ const m = String(res.source).match(/^(.+):(\d+)$/);
458
+ if (!m) return null;
459
+ const line = parseInt(m[2], 10);
460
+ if (res.source_rel) {
461
+ const rel = res.source_rel.replace(/:\d+$/, '');
462
+ await ensureBuffer(rel); // peek/jump needs the target model to exist
463
+ return {
464
+ uri: monaco.Uri.from({ scheme: 'ruby-ide', path: '/' + rel }),
465
+ range: new monaco.Range(line, 1, line, 1),
466
+ };
467
+ }
468
+ setStatus(`defined at ${m[1]}:${line} (outside workspace)`, { sticky: true });
469
+ return null;
470
+ },
471
+ });
472
+
473
+ monaco.editor.registerEditorOpener({
474
+ openCodeEditor: (_source, resource, selectionOrPosition) => {
475
+ const path = resource.path.replace(/^\//, '');
476
+ if (!buffers.has(path) && !fileList.find(f => f.path === path)) return false;
477
+ openFile(path).then(() => {
478
+ let pos = selectionOrPosition;
479
+ if (pos && pos.startLineNumber) pos = { lineNumber: pos.startLineNumber, column: pos.startColumn };
480
+ if (pos) { editor.setPosition(pos); editor.revealLineInCenter(pos.lineNumber); }
481
+ });
482
+ return true;
483
+ },
484
+ });
485
+
486
+ // ---------------------------------------------------- diagnostics + live run
487
+ let runtimeMarker = [];
488
+ const refreshDiagnostics = async () => {
489
+ const model = editor.getModel();
490
+ if (!model) return;
491
+ const res = await post('/api/diagnostics', { code: model.getValue() });
492
+ const markers = (res.diagnostics || []).map(d => ({
493
+ severity: monaco.MarkerSeverity.Error,
494
+ message: `syntax: ${d.message}`,
495
+ startLineNumber: d.line, startColumn: 1,
496
+ endLineNumber: d.line, endColumn: model.getLineMaxColumn(Math.min(d.line, model.getLineCount())),
497
+ }));
498
+ monaco.editor.setModelMarkers(model, 'ruby-ide', markers.concat(runtimeMarker));
499
+ };
500
+
501
+ let decorations = [];
502
+ const renderRun = (res) => {
503
+ const model = editor.getModel();
504
+ if (!model) return;
505
+
506
+ // ---- side panel
507
+ const finals = res.final_locals || {};
508
+ const rows = Object.entries(finals).map(([v, info]) =>
509
+ `<tr><td class="var">${esc(v)}</td><td class="klass">${esc(info.class)}</td><td class="val">${esc(info.value)}</td></tr>`);
510
+ $('state').innerHTML = rows.join('');
511
+ $('state-empty').style.display = rows.length ? 'none' : 'block';
512
+ $('stdout').textContent = res.stdout || '';
513
+ const errParts = [];
514
+ if (res.bundler_error) errParts.push(`bundler: ${res.bundler_error.message}`);
515
+ if (res.error) errParts.push(`${res.error.class || ''} ${res.error.message || res.error}`.trim());
516
+ $('run-error').textContent = errParts.join('\n\n');
517
+
518
+ // ---- runtime error squiggle
519
+ runtimeMarker = (res.error && res.error.line) ? [{
520
+ severity: monaco.MarkerSeverity.Error,
521
+ message: `runtime: ${res.error.class}: ${res.error.message}`,
522
+ startLineNumber: res.error.line, startColumn: 1,
523
+ endLineNumber: res.error.line,
524
+ endColumn: model.getLineMaxColumn(Math.min(res.error.line, model.getLineCount())),
525
+ }] : [];
526
+
527
+ // ---- per-line "debugger" annotations: show what CHANGED on each line
528
+ const states = res.line_states || {};
529
+ const lines = Object.keys(states).map(Number).sort((a, b) => a - b);
530
+ let prev = {};
531
+ const newDecos = [];
532
+ for (const ln of lines) {
533
+ if (ln > model.getLineCount()) continue;
534
+ const cur = states[ln];
535
+ const changed = Object.entries(cur).filter(([v, info]) =>
536
+ !prev[v] || prev[v].value !== info.value);
537
+ const shown = changed.filter(([, info]) => info.class !== 'NilClass');
538
+ if (shown.length) {
539
+ const txt = shown.map(([v, info]) => `${v}: ${info.class} = ${info.value}`).join(' ');
540
+ const col = model.getLineMaxColumn(ln);
541
+ newDecos.push({
542
+ range: new monaco.Range(ln, col, ln, col),
543
+ options: {
544
+ showIfCollapsed: true, // collapsed range: without this, injected text is dropped
545
+ after: { content: ' # ' + txt.slice(0, 120), inlineClassName: 'inline-state' },
546
+ },
547
+ });
548
+ }
549
+ prev = cur;
550
+ }
551
+ decorations = editor.deltaDecorations(decorations, newDecos);
552
+ refreshDiagnostics();
553
+ };
554
+
555
+ // Responses can arrive out of order (a buffer with a slow network call
556
+ // takes seconds; the next keystroke's run returns first) — only the
557
+ // newest request may render, and only onto the model it was sent for.
558
+ let runSeq = 0;
559
+ const runBuffer = async () => {
560
+ const model = editor.getModel();
561
+ if (!model) return;
562
+ const seq = ++runSeq;
563
+ setStatus('evaluating…');
564
+ try {
565
+ const res = await post('/api/run', { code: model.getValue(), path: currentPath });
566
+ if (seq !== runSeq || editor.getModel() !== model) return; // superseded
567
+ renderRun(res);
568
+ setStatus(res.error ? 'evaluated with error' : 'evaluated ✓');
569
+ } catch (e) { setStatus('server unreachable'); }
570
+ };
571
+
572
+ const debouncedDiag = debounce(refreshDiagnostics, 300);
573
+ const liveRun = debounce(() => { if ($('live').checked) runBuffer(); }, 700);
574
+ editor.onDidChangeModelContent(() => { debouncedDiag(); liveRun(); renderFileList(); });
575
+
576
+ $('run').addEventListener('click', runBuffer);
577
+
578
+ $('learn').addEventListener('click', async () => {
579
+ setStatus('running learn commands…', { sticky: true });
580
+ const res = await post('/api/learn', {});
581
+ const out = [];
582
+ if (res.error) out.push(typeof res.error === 'string' ? res.error : `${res.error.class}: ${res.error.message}`);
583
+ (res.commands || []).forEach(c => {
584
+ out.push(`$ ${c.command}${c.timeout ? ' [TIMED OUT]' : c.ok ? '' : ` [exit ${c.exit}]`}\n${(c.output || '').trim()}`);
585
+ });
586
+ if (res.methods_observed !== undefined) out.push(`→ learned types for ${res.methods_observed} methods`);
587
+ $('tests-out').textContent = out.join('\n\n');
588
+ $('tests-out').style.display = 'block';
589
+ $('tests-empty').style.display = 'none';
590
+ setStatus(res.error
591
+ ? 'learn failed'
592
+ : `learn ${res.passed ? 'passed' : 'FAILED'} — ${res.methods_observed} methods observed`,
593
+ { sticky: true });
594
+ });
595
+
596
+ // ------------------------------------------------------------------ boot
597
+ (async () => {
598
+ await refreshFiles();
599
+ const first = fileList.find(f => f.path === 'main.rb') || fileList[0];
600
+ if (first) {
601
+ await openFile(first.path);
602
+ } else {
603
+ editor.setModel(monaco.editor.createModel(
604
+ "# empty workspace — create a file with the + button\n", 'ruby'));
605
+ }
606
+ setStatus('ready');
607
+ })();
608
+ });
609
+ </script>
610
+ </body>
611
+ </html>
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 - present Microsoft Corporation
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.