├── .gitignore
├── LICENSE
├── README.md
├── REPLugger LIVE 2018.txt
├── build_mock_environment.sh
├── compile.sh
├── electron_app.js
├── index.html
├── mock_environment.js
├── new_index.html
├── package.json
├── splash_video.txt
└── staged_src
├── .babelrc
├── index.html
├── interpreter.js
├── interpreter.py
├── main.js
├── renderer.js
├── style.less
└── visualizations.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # REPLugger-specific
2 | /mock_environment
3 | /last_run.js
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (http://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # Typescript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 |
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Glen Chiacchieri
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | REPLugger: a pleasant and scalable live code editor prototype
2 | =============================================================
3 |
4 | REPLugger is a research prototype code editor that integrates the features of a REPL and a debugger directly into the editing experience. **[See the 10 minute demo video here](https://youtu.be/F8p5bj01UWk).**
5 |
6 | You can view [my website](http://glench.com) to see more of my projects. You can also sign up for my [UI Design for the Future of Programming newsletter](https://tinyletter.com/Flowsheets) to be updated about my research and analysis of programming.
7 |
8 | A note: REPLugger is research code meant to demonstrate an idea, not to be well-engineered. You're probably better off trying to invent your own way of making the features possible than to figure out how I did it.
9 |
10 | ## Installing
11 |
12 | `npm install` / `yarn`
13 |
14 | ## Running
15 |
16 | `npm start` / `yarn start`
17 |
18 | I would not recommend trying to run this project since it was made for demo purposes only, not to be a robust editing experience.
19 |
--------------------------------------------------------------------------------
/REPLugger LIVE 2018.txt:
--------------------------------------------------------------------------------
1 | REPLugger LIVE 2018 Talk
2 |
3 |
4 | Hi! I'll be presenting REPLugger, a live code editor, but first I want to give just some context for where it came from.
5 |
6 | It's been about 7 years since Bret Victor came out with his amazing programming demos in his presentation Inventing on Principle and his essay Learnable Programming.
7 |
8 | From a programming perspective, I think the main thing Bret did with these demos is show us one of the biggest and most under-appreciated problems with programming — that programmers can't see what their programs are doing. Programmers look at a bunch of text and have to imagine most of what the program does in their heads. And his demos not only pointed out this problem but provided some plausible solutions — showing concrete data in the editing experience.
9 |
10 |
11 |
12 | This sounds like a great research direction to me, but I wanted to see for myself. After working with Bret for several years, I made Flowsheets v2 in 2017. This was a prototype programming environment for creating small data transformation programs. Its main idea was that you always saw the data associated with the code, kind of like a spreadsheet or a reactive notebook, though it felt distinct from these. Even though it was just a prototype, it quickly became my preferred programming environment, mostly because it felt so great to see the data as I programmed. Instead of blindly typing code and guessing if it worked, I had concrete evidence that the code did what I expected. Having this evidence gave me an informed sense of confidence in my code, and as a result programming basically just felt *good*, it felt nice!
13 |
14 | So from using Flowsheets I verified that seeing data as I programmed actually can feel good in practice. But Flowsheets was only designed for making small programs — showing all the data in a large program would be overwhelming. So I began to wonder, how could runtime data be brought into the programming exprience in large and complex systems, ones that professional programmers tend to work on? What data is actually useful for these programmers to see when they're creating large systems?
15 |
16 | I built REPLugger, the prototype I'm about to show you, as one possible answer to these questions.
17 |
18 | But before I show you REPLugger, I first wanted to address three of the main criticisms of programming research projects:
19 |
20 | - "this only works for toy programs" - not representative of real programs
21 | - "this is fine for beginners but not me" - doesn't address actual intermediate or expert programmer concerns
22 | - "this isn't useful enough for me to adopt" - not practical
23 |
24 | To me, all these criticisms point to one underlying concern: that programming research doesn't adequately address the lived human experience of programming, a concern I share.
25 |
26 | So with REPLugger I tried to address these by
27 |
28 | - testing it with the code for Flowsheets, a real program I made with a few thousand lines of JS
29 | - using it myself
30 | - and seeing empirically if it actually provided tangible benefits
31 |
32 | My hope is that addressing these criticisms, I can help bring some of the promise we saw in Bret Victor's demos to reality, to improve the everyday experience of the 20 million or so programmers of the world, and the countless number of potential programmers in the future.
33 |
34 | I made this demo for
35 | - an advanced programmer (not beginners)
36 | - writing (not reading)
37 | - a real program (not a toy program)
38 | - to see what concrete program data is empirically useful when programming
39 |
40 | wanted an empirical approach in some ways and a suggstive approach in other ways
41 |
42 |
43 | [REPLugger intro]
44 |
45 | So with the context out of the way, this is REPLugger.
46 |
47 | On the left there's a text editor. In this case I'm editing a real file from the Flowsheets codebase.
48 |
49 | On the right we have a table listing scopes, names, and values from a sample run of the program. Kind of like a debugger.
50 |
51 | And on the bottom we show the output of the line the cursor is on. Kind of like a REPL. Get it? REPL and a debugger makes REPLugger.
52 |
53 | Anyway, changing the cursor changes the debugger and output UIs. You can see as I move the cursor, the output at the bottom changes, and names are added to the table. REPLugger basically tries to run the code up to the current line.
54 |
55 | These UIs also changed as I type. If I type 1+1, the REPL shows the result of that expression.
56 |
57 | for example, 1+1 = 2
58 |
59 | Like any other REPL, I can do little experiments and try things out. But unlike most REPLs, the results are just right there, available as I type in my text editor, without switching windows or having to do anything. Also unlike most REPLs, REPLugger has access to the entire program's state without having to laboriously copy and paste code.
60 |
61 | For example, if I wanted to see how many cells there were in this grid I can just type
62 |
63 | rows * columns
64 |
65 | And there's the total number of cells. And if I found I wanted to use this value in my program, I could just assign it to a variable.
66 |
67 | var num_cells = rows * columns;
68 |
69 | And it shows up here in the debugger.
70 |
71 | The idea of REPLugger is that the REPL and debugger live alongside your code editor so you're never programming blind. You can see all the values in the program, get feedback as you type, try experiments like you would in a REPL, etc. And most of this happens without having to interact with the UI, you just type code as normal and the UIs are available if you want to use them. From my experience watching people program, I think something like this will not only prevent simple bugs, but make programming just feel more plasant.
72 |
73 | Let's do a little simple programming to see what I mean.
74 |
75 |
76 | [Programming with REPLugger]
77 |
78 | First I want to add a little code that changes the background's height based on the window height. so I start writing my if-statement
79 |
80 | in initialize_grid:
81 |
82 | if (window.innerHeight > height) {
83 |
84 | }
85 |
86 |
87 | We see something interesting here. I haven't finished writing the expression but REPLugger still finds a valid expression to run and shows me what that evaluates to. The point of this is to give me feedback as I type so that I can be assured my code is actually doing what I imagine it's doing. So I'll finish writing the expression... and the REPL continues to show me that this line evaluates to true.
88 |
89 | If I wanted to check why it evaluates to true, I could just highlight parts of the expression and REPLugger shows me the value of that part.
90 |
91 | But let's continue writing and make the background height twice the window's height.
92 |
93 | height = window.innerHeight * 2;
94 |
95 | Again we see the value updated in the REPL and the table.
96 |
97 | I have to say, programming with this kind of feedback feels nice. This idea comes directly out of my experience with programming — often, I simply want to know — does the current line I'm writing work? I can't know in most programming environments or I have to jump through hoops to print out the value. It's really frustrating! But in REPLugger, it's right there.
98 |
99 | of course, a lot of this is UI code, so we'd probably want to run the program to see how it looks visually. [ run code ] yeah, looks right
100 |
101 | While it would probably be nice to see the visual output of the program change as I type, I wanted REPLugger to be able to work for programs with and without UIs, so I chose to have REPLugger work at the symbolic level of the code instead of the visual level of the UI.
102 |
103 | Let's see more of how that works.
104 |
105 |
106 |
107 | [Filling in example arguments]
108 |
109 | The main idea of REPLugger is you can always see concrete values.
110 |
111 | In some code, like in functions with abstract arguments, REPLugger doesn't have any values to show.
112 |
113 | To demonstrate this, we'll make a new function, clamp, that takes three arguments. When we do this, we see that REPLugger highlights those arguments in red, showing they don't have values. So what we can do is click in these cells and fill in some values. [1 2 10] They turn yellow to show they've been manually edited.
114 |
115 | And then we can write the rest of the function and we'll have concrete feedback as we do it...
116 | if (num < min) {
117 | return min;
118 | } else if (num > max) {
119 | return max;
120 | }
121 | return num;
122 |
123 | Clamp is a simple function so we don't really need much feedback here. But we can use the feedback to play around and try things out.
124 |
125 | So what if we tried to clamp null between 1 and 2? 1? huh? Let's see why that happened. We can go back into the function and manually edit the argument here and see that in this first comparison it thinks null is less than 1. Wow, thank you javascript.
126 |
127 | So again this concrete feedback comes in handy, not just as we're writing code but as we're reading it, especially when we have the ability to manually edit data in the program to see what would happen in different cases. Although seeing the results of one line at a time when reading code isn't very useful — REPLugger was made more for writing code.
128 |
129 | So say we felt like this null case was important and that we wanted to make sure we were aware of when we went back to the code later. We can go to this dropdown over here and save this set of arguments and name it. We see a little number appear here in this dropdown showing that there is one saved case.
130 |
131 | If we change this argument back to what we had before and save that too, we call it default. So now we have two cases. Default is the one shown right now, but by selecting the other it changes the value. And I can switch back.
132 |
133 | So imagine I leave the program and come back in a few months when I forgot about it. When I click in here I see, oh, there are two cases I wanted myself to know about when looking at this function. There's this case, and there's this case. Thanks, past me!
134 |
135 |
136 | Or imagine I'm on a team of programmers all using REPLugger. I wrote this function and saved these two examples in source control. So when a different team member comes into this function and is using REPLugger, they can see those examples. These saved examples provide additional context for what I had in mind when writing this code, what conditions I expected this code to run under, almost like user-facing unit tests.
137 |
138 | We've all tried to read unfamiliar code and had questions like, "why did the person write it like this? how did they expect this to work?" And we can't really answer those questions because they're not in the code. By having these filled-in examples in REPLugger, we not only get live feedback as we're writing the code, but also some context when we're reading or modifying the code later.
139 |
140 | But clamp is just a simple example I'm using to demonstrate the features of REPLugger. Where I think this stuff is more useful is more complicated code. So let's look at that now.
141 |
142 |
143 | [Deeper nested code]
144 |
145 | So if we come down here we can see we're down in a few different scopes. This code handles mouse movements, mostly when clicking and dragging parts of boxes in Flowsheets.
146 |
147 | First we see this yellow evt parameter. I previously filled in this evt parameter with an example. Clicking in here we see that this value was created from a short expression I wrote. When I hit enter, it evaluates the expression and shows the value.
148 |
149 | new window.MouseEvent('mousemove'); evt.pageX = 0; evt.pageY = 0;
150 |
151 | But here's something else: REPLugger is saying there's an error. And all the values in this table are blank. If we look here, we see the code is looking for resize_drag to have a truthy value but resize_drag is null! So REPLugger ignored the if-statement in order to try to run up until where the cursor is.
152 |
153 | So what we can do is come to this dropdown and choose "add name". We can call this resize_drag and give it a value:
154 |
155 | new Resize_Drag(create_ui_block())
156 |
157 | So what this is doing is saying... "ignore whatever the value of resize_drag was and instead make it this value, but only in this scope". We're overwriting the value temporarily to get the program into a useful state.
158 |
159 | And we see that by doing that all the variables in this table now have values and the error went away. Using this overwrite, we put the program in a state.
160 |
161 | I think overwriting is a really exciting feature. We can overwrite any part of the program with any values we want.
162 |
163 | So right here we can say, I don't want dx to be that, I want to see what would happen when it's 10, or the same as cell_width.
164 |
165 | The idea for overwrites comes out of my experience as a software engineer. Often, I'd suspect bugs were coming from a program because of a particular state, but had no way of getting the program into that state or that codepath in order to check.
166 |
167 | In fact, I'd say a lot of what makes programming difficult is that programs can get into many different states and code has to handle those many states. To help programmers with that, we give them explicit control over the states so they can see for themselves how their code works under different conditions. This can be especially useful in debugging where often we have hypotheses about what's going on in our code but it's not straightforward to test that hypothesis and get feedback. Overwrites in REPLugger are an attempt to let programmers quickly and fluidly try their hypotheses and get an understanding of how large programs work in different conditions.
168 |
169 |
170 | Our tools are designed to solve problems. To borrow an analogy from Bret Victor, looking at a hammer you can see - oh okay, focusing human force. The hard part is the "hard part" the hammerhead.
171 |
172 | if you look at programming environments today and try to reason backward to figure out what the difficult parts of programming are, you'd come to the conclusion that finding and manipulating text. But this obviously isn't the hard part of programming. The hard part is understanding how pieces fit together.
173 |
174 | a lot of debugging depends on intuition, and intuition depends on data. "we have this value, this value, and this value" -> intuition -> oh this one thing explains. one problem with print statements is that you have to beg for information. it completely precludes the idea of discovery. and getting the program to give us this information is often arduous
175 |
176 |
177 |
178 |
179 |
180 |
181 | [Getting runtime data]
182 |
183 | freezing program state, importing it into REPLugger
184 |
185 |
186 | [TODO]
187 |
188 | time?
189 | show more data? (chrome debugger)
190 | visualization types? tables? ipython notebooks?
191 | solid model for sample environment?
192 |
193 |
194 | [Things to take away]
195 |
196 | concrete data to bring clarity
197 |
198 | when writing lines
199 | when reading code
200 |
201 | feedback when writing current line
202 | showing whole program context
203 |
204 | the ability to try things out, save
205 | making states explicit and manipulable
206 |
207 | calm experience
208 | When I showed REPLugger to two friends, both remarked on how calm the experience felt. There's very little interaction necessary. Instead of frantically switching between windows, REPLugger's extra UI provides a stable frame for answering questions about program state. When I call REPLugger "pleasant", this is part of what I mean. It just feels good.
209 |
210 | overwrites - ways of working with states of program interactively, show context
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | ----------------------------------------
219 | ----------------------------------------
220 | ----------------------------------------
221 | ----------------------------------------
222 | ----------------------------------------
223 | ----------------------------------------
224 | ----------------------------------------
225 | ----------------------------------------
226 | ----------------------------------------
227 | ----------------------------------------
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | it's been almost 7 years since Bret's inventing on principle, where he demonstrated an inspiring direction to improve programming — showing program data. yet I still haven't seen any compelling examples of how that work could be done in a practical way.
236 |
237 | also felt some criticisms about programming research projects, including Bret's work:
238 | - made for beginner programmers,
239 | - they only work for toy problems
240 | - they don't provide enough utility for programmers to adopt them
241 |
242 | Like many of us here, I'm inspired by Bret's work, but offers more of a good framing of the problem and provocative examples of what else could be done, but not solutions. So I wanted a solution that actually provided tangible benefits.
243 |
244 | inspired by Bret
245 | flowsheets felt good
246 | With Flowsheets, it felt good! It wasn't based on principle — it just felt good. it was biased
247 |
248 | no theorizing — what feedback is actually useful to programmers? I examined my own experience and Jonathan Blow and found that most of the time I just wanted some feedback to see if the line I just wrote worked as I expected. and same goes for reading code - just want to see an example of what the thing is doing (often don't need to know its general behavior)
249 |
250 |
251 | common underlying criticism: programming research doesn't actually address the human experience of programming. good that researchers are divorced from everyday experience since it allows for greater freedom of ideas, but gotta come back into the realm of human experience at some point. and my argument is that doing this actually provides a lot of inspiration. some of the best scientists are those working at the intersection of ideas and human experience
252 |
253 |
254 |
255 | Watched Jonathan Blow, show clips of him going "wait what's happening?"
256 |
257 | stop arguing on principles, argue on evidence!
258 |
259 | new feature:
260 | replugger() captures state
261 | actually do some programming to show how it works in practice
262 | - add number of overwrite groups as badge to dropdown, to show other programmers there are things to check out
263 |
264 |
265 | REPLugger is calm - provides stable context, not fleeting like many programs running through time. also calm since there's no flipping back and forth between windows.
266 |
267 |
268 | design values - be clear about what circumstances an intervention was designed for, who it would help, and when it's appropriate
269 |
270 |
271 | get across that this is USEFUL WHILE PROGRAMMING
272 | show the team-based programming thing more clearly
273 |
274 | show the data
275 |
276 | look on programming environment? What does this interface say about what's hard about programming? text editors and IDEs say the hard thing is about finding and manipulating text.
277 |
278 | show concrete example of how overwrites can be useful to programmers
279 | show example of how states can be useful to other programmers
280 |
--------------------------------------------------------------------------------
/build_mock_environment.sh:
--------------------------------------------------------------------------------
1 | mkdir -p mock_environment
2 | cd staged_src/
3 | cp * ../mock_environment/
--------------------------------------------------------------------------------
/compile.sh:
--------------------------------------------------------------------------------
1 | cd staged_src/ && babel . --out-dir ../mock_environment/ && cp interpreter.py ../mock_environment/
--------------------------------------------------------------------------------
/electron_app.js:
--------------------------------------------------------------------------------
1 | const {app, BrowserWindow} = require('electron')
2 | const path = require('path')
3 | const url = require('url')
4 |
5 | // Keep a global reference of the window object, if you don't, the window will
6 | // be closed automatically when the JavaScript object is garbage collected.
7 | let win
8 |
9 | function createWindow () {
10 | // Create the browser window.
11 | win = new BrowserWindow({width: 1400, height: 900})
12 |
13 | // and load the index.html of the app.
14 | win.loadURL(url.format({
15 | pathname: path.join(__dirname, 'new_index.html'),
16 | protocol: 'file:',
17 | slashes: true
18 | }))
19 |
20 | // Open the DevTools.
21 | win.webContents.openDevTools()
22 |
23 | // Emitted when the window is closed.
24 | win.on('closed', () => {
25 | // Dereference the window object, usually you would store windows
26 | // in an array if your app supports multi windows, this is the time
27 | // when you should delete the corresponding element.
28 | win = null
29 | })
30 | }
31 |
32 | // This method will be called when Electron has finished
33 | // initialization and is ready to create browser windows.
34 | // Some APIs can only be used after this event occurs.
35 | app.on('ready', createWindow)
36 |
37 | // Quit when all windows are closed.
38 | app.on('window-all-closed', () => {
39 | // On macOS it is common for applications and their menu bar
40 | // to stay active until the user quits explicitly with Cmd + Q
41 | if (process.platform !== 'darwin') {
42 | app.quit()
43 | }
44 | })
45 |
46 | app.on('activate', () => {
47 | // On macOS it's common to re-create a window in the app when the
48 | // dock icon is clicked and there are no other windows open.
49 | if (win === null) {
50 | createWindow()
51 | }
52 | })
53 |
54 | // In this file you can include the rest of your app's specific main process
55 | // code. You can also put them in separate files and require them here.
--------------------------------------------------------------------------------
/mock_environment.js:
--------------------------------------------------------------------------------
1 | const parse_flow_src = require('flow-parser');
2 | const flow_remove_types = require('flow-remove-types');
3 |
4 |
5 | function prepare_current_file_to_act_as_server(file_src, names_of_interest) {
6 | var server_src = `console.log('server would be running here')`
7 | var mock_server_src = `${file_src.replace(/const /g, 'var ')}
8 |
9 | /* ----- REPLUGGER MOCK SERVER ----- */
10 |
11 | const replugger_circular_json = require('circular-json');
12 | const replugger_undescore = require('underscore');
13 | function replugger_json_replacer(key, value) {
14 | if (typeof value === 'function') {
15 | return value.toLocaleString();
16 | }
17 | return value;
18 | }
19 | var replugger_output = {
20 | ${names_of_interest.map(name => `${name}: replugger_circular_json.stringify(${name}, replugger_json_replacer).slice(0, 100)`).join(',\n')}
21 | }
22 | process.stdin.setEncoding('utf-8')
23 | process.stdin.on('data', function(buffer) {
24 | var lines = buffer.toString().split('\\n');
25 | for (var i = 0; i < lines.length; ++i) {
26 | var data = lines[i];
27 | if (data.startsWith('replugger_summary_info:')) {
28 | var name = data.replace(/^replugger_summary_info:/, '');
29 | try {
30 | var output = eval(name)
31 | if (typeof output === 'object' && output !== null && output.isTrusted === false) {
32 | output.isTrusted = true;
33 | }
34 | var json_output = replugger_circular_json.stringify(output, replugger_json_replacer);
35 | if (json_output === undefined) {
36 | process.stdout.write('undefined\\n')
37 | } else {
38 | process.stdout.write(json_output.slice(0, 100).replace(/\\n/g, '__REPLUGGERNL__')+'\\n')
39 | }
40 | } catch(e) {
41 | process.stdout.write('replugger_error:'+e.toString().replace(/\\n/g, '__REPLUGGERNL__')+'\\n')
42 | }
43 | } else if (data.startsWith('replugger_full_info:')) {
44 | var name = data.replace(/^replugger_full_info:/, '');
45 | try {
46 | var output = eval(name);
47 | // why is typeof null an object?!?!?!
48 | if (output !== null && typeof output === 'object') {
49 | var json_output = '{';
50 | _.each(output, function(value, key) {
51 | var value = replugger_circular_json.stringify(value, replugger_json_replacer)
52 | json_output += '"'+ key +'": '+ (value !== undefined ? value.slice(0, 100) : 'undefined') + ',,'
53 | })
54 | json_output += '}'
55 | } else {
56 | try {
57 | json_output = replugger_circular_json.stringify(output, replugger_json_replacer).slice(0,500);
58 | } catch(e) {
59 | json_output = 'undefined'
60 | }
61 | }
62 | process.stdout.write(json_output.replace(/\\n/g, '__REPLUGGERNL__')+'\\n')
63 | } catch(e) {
64 | process.stdout.write('replugger_error:'+e.toString().replace(/\\n/g, '__REPLUGGERNL__')+'\\n')
65 | }
66 | } else if (data.startsWith('replugger_run_code:')) {
67 | var code = data.replace('replugger_run_code:', '').replace(/__REPLUGGERNL__/g, '\\n');
68 | try {
69 | eval(code);
70 | process.stdout.write('ok\\n')
71 | } catch(e) {
72 | process.stdout.write('replugger_error:'+e.toString().replace(/\\n/g, '__REPLUGGERNL__')+'\\n')
73 | }
74 | }
75 | }
76 | });
77 | process.stdout.setEncoding('utf-8')
78 | process.stdout.write('ok\\n');
79 | `
80 | return mock_server_src;
81 | }
82 | module.exports.prepare_current_file_to_act_as_server = prepare_current_file_to_act_as_server;
83 |
84 | function scopes_and_names(file_src, line_number) {
85 | var scopes = [];
86 | /* e.g.
87 | [
88 | {scope_name: 'function (evt) {',
89 | line_number: 102,
90 | names_in_scope: ['evt', 'dx', 'dy'],
91 | fill_in_names: ['evt'],
92 | }
93 | ]
94 | */
95 |
96 | var lines = file_src.split('\n');
97 | var scope_stack = [{scope_name: 'file', line_number: null, names_in_scope: [], fill_in_names: []}];
98 | for (var i = 0; i < lines.length; ++i) {
99 | if (i >= line_number) {
100 | return scope_stack;
101 | }
102 |
103 | var line = lines[i].trim();
104 | if (line.startsWith('const ') || line.startsWith('var ')) {
105 | var name = line.split('=')[0].replace(/(const|var)/g, '').trim();
106 | scope_stack[scope_stack.length-1].names_in_scope.push(name)
107 | // } else if (line.startsWith('function') && scope_stack.length == 1) {
108 | // // defining global function
109 | } else if (line.startsWith('class ') && scope_stack.length == 1) {
110 | var name = line.replace(/ \{.*/, '').replace('class ', '')
111 | scope_stack[0].names_in_scope.push(name);
112 | } else if (line.includes('this.') || line.includes('this,')) {
113 | var current_scope_stack = scope_stack[scope_stack.length-1];
114 | if (!current_scope_stack.names_in_scope.includes('this')) {
115 | current_scope_stack.names_in_scope.push('this')
116 | }
117 | } else if (line.startsWith('function ') && scope_stack.length == 1) {
118 | var name = line.replace(/\(.*\) \{.*/, '').replace('function ', '');
119 | scope_stack[0].names_in_scope.push(name);
120 | } else if (line.startsWith('}')) {
121 | if (scope_stack.length > 1) {
122 | scope_stack.pop();
123 | }
124 | }
125 | if (line.endsWith('{')) {
126 | scope_stack.push({scope_name: line, line_number: i+1, names_in_scope: [], fill_in_names: []})
127 | var current_scope_stack = scope_stack[scope_stack.length-1];
128 |
129 | var function_re = /function[\w\s]*\(([\w,\s]+)\)\s?\{$/;
130 | var match_groups = line.match(function_re)
131 | if (match_groups) {
132 | var arguments = match_groups[match_groups.length-1].split(',').map(arg => arg.trim())
133 | arguments.forEach(function(arg) {
134 | current_scope_stack.names_in_scope.push(arg)
135 | current_scope_stack.fill_in_names.push(arg)
136 | })
137 | }
138 | }
139 | }
140 | }
141 | module.exports.scopes_and_names = scopes_and_names;
142 |
143 | function add_parens(src) {
144 | // thing.map(function () {
145 | // -> thing.map(function() {})
146 | var length = src.length;
147 | var paren_queue = [];
148 | for (var i = 0; i < length; ++i) {
149 | var character = src[i];
150 | if (character == '(') {
151 | paren_queue.push(')')
152 | } else if (character == '{') {
153 | paren_queue.push('}')
154 | } else if (character == ')' || character == '}') {
155 | paren_queue.pop();
156 | }
157 | }
158 | return src+'\n'+ paren_queue.reverse().join('');
159 | }
160 | module.exports.add_parens = add_parens;
161 |
162 | function ast_to_list_of_scopes(node, scope_name, current_line_number, src, output) {
163 | // produce a list of scopes with variable names in each scope:
164 | // [
165 | // {name: 'file': names_in_scope: ['module', 'clamp']},
166 | // {name: 'function initialize_grid() {', names_in_scope: ['width', 'height']}]
167 | // ]
168 |
169 | var scope = {name: scope_name, names_in_scope: []}
170 |
171 | var nodes = null;
172 | if (node.type == 'Program') {
173 | scope.names_in_scope.push('module')
174 | nodes = node.body;
175 | } else if (node.type == 'FunctionDeclaration' || node.type == 'FunctionExpression') {
176 | node.params.forEach(param => scope.names_in_scope.push(param.name))
177 | nodes = node.body.body;
178 | } else if (node.type == 'BlockStatement') {
179 | nodes = node.body;
180 | } else if (node.type == 'CallExpression') {
181 | nodes = node.arguments; //node.arguments[node.arguments.length-1].body;
182 | } else if (node.type == 'IfStatement') {
183 | nodes = node.consequent.body;
184 | }
185 |
186 | if (!nodes) return output;
187 |
188 |
189 | // find declarations
190 | nodes = nodes.filter(decl => decl.loc.start.line <= current_line_number);
191 | nodes.forEach(decl => {
192 | switch (decl.type) {
193 | case 'VariableDeclaration':
194 | decl.declarations.forEach(inner_decl => {
195 | scope.names_in_scope.push(inner_decl.id.name)
196 | })
197 | return;
198 | // case 'ExpressionStatement':
199 | // return;
200 | case 'ClassDeclaration':
201 | scope.names_in_scope.push(decl.id.name);
202 | return;
203 | case 'FunctionDeclaration':
204 | case 'FunctionExpression':
205 | if (decl.id) {
206 | scope.names_in_scope.push(decl.id.name)
207 | }
208 | return;
209 | }
210 | })
211 |
212 | if (scope.names_in_scope.length > 0) {
213 | output.push(scope);
214 | }
215 |
216 | const last_node = nodes[nodes.length-1];
217 | if (!last_node) return output;
218 | if (last_node.loc.start.line == current_line_number && last_node.loc.end.line == current_line_number) {
219 | return output;
220 | }
221 |
222 | var current_line_src = src.split('\n')[last_node.loc.start.line-1];
223 |
224 | if (last_node.type == 'FunctionDeclaration' || last_node.type == 'FunctionExpression') {
225 |
226 | ast_to_list_of_scopes(last_node, current_line_src, current_line_number, src, output);
227 | } else if (last_node.type == 'ExpressionStatement') {
228 | ast_to_list_of_scopes(last_node.expression, current_line_src, current_line_number, src, output);
229 | } else if (last_node.type == 'CallExpression') {
230 | ast_to_list_of_scopes(last_node.arguments[last_node.arguments.length-1], current_line_src, current_line_number, src, output);
231 | } else if (last_node.type == 'IfStatement') {
232 | // find which branch of the if statement contains the current line
233 | var test_node = last_node;
234 | while (test_node.type == 'IfStatement') {
235 | if (test_node.consequent.loc.start.line <= current_line_number && test_node.consequent.loc.end.line >= current_line_number) {
236 | break
237 | }
238 | test_node = test_node.alternate;
239 | }
240 | ast_to_list_of_scopes(test_node, src.split('\n')[test_node.loc.start.line-1], current_line_number, src, output);
241 | } else if (last_node.type == 'FunctionExpression') {
242 | ast_to_list_of_scopes(last_node.body, current_line_src, current_line_number, src, output);
243 | } else if (last_node.type == 'BlockStatement') {
244 | ast_to_list_of_scopes(last_node.body, current_line_src, current_line_number, src, output);
245 | } else {
246 | // console.log('error-causing node:', last_node)
247 | // throw 'unhandled AST node type: '+last_node.type;
248 | }
249 |
250 | return output;
251 | }
252 |
253 |
254 |
255 | function ast_node_to_js(node, scopes) {
256 | if (!node.type) { // array of nodes
257 | var nodes = node;
258 | var output = '';
259 | var done = false;
260 | nodes.forEach(node => {
261 | if (done) { return; }
262 | if (scopes && node.loc.end.line == current_line_number && node.loc.start.line == current_line_number) {
263 | output += `replugger_values = {};\n${scopes.map(scope => { return scope.names_in_scope.map(name => `try { replugger_values['${name}'] = ${name} } catch(e) { replugger_values['${name}'] = '...' }`).join('\n')}).join('\n')}\n${ast_node_to_js(node, scopes)}`
264 | done = true;
265 | return
266 | }
267 | output += ast_node_to_js(node, scopes)+'\n'
268 | })
269 | return output;
270 | }
271 | switch (node.type) {
272 | case 'Program':
273 | return `try {
274 | ${ast_node_to_js(node.body, scopes)}
275 | } catch (e) {
276 | replugger_values = {};
277 | ${scopes.map(scope => { return scope.names_in_scope.map(name => `try { replugger_values['${name}'] = ${name} } catch(e) { replugger_values['${name}'] = '...' }`).join('\n')}).join('\n')}
278 | }
279 | `
280 | case 'ExpressionStatement':
281 | if (node.loc.start.line < current_line_number && node.loc.end.line >= current_line_number) {
282 | var test_node = node.expression;
283 | // find the part of this expression that the cursor is in
284 | while (test_node.type == 'CallExpression') {
285 | test_node = test_node.arguments[test_node.arguments.length-1];
286 | }
287 | return `// function as argument
288 | ${ast_node_to_js(test_node, scopes)}`
289 | }
290 | return ast_node_to_js(node.expression, scopes);
291 | case 'ClassDeclaration':
292 | return `class ${ast_node_to_js(node.id, scopes)} ${ast_node_to_js(node.body, scopes)}`;
293 | case 'ClassProperty':
294 | return ``;
295 | case 'MethodDefinition':
296 | return `${ast_node_to_js(node.key, scopes)}(${node.value.params.map(param => param.name).join(', ')}) ${ast_node_to_js(node.value.body, scopes)}`;
297 | case 'ClassBody':
298 | case 'BlockStatement':
299 | return `{
300 | ${ast_node_to_js(node.body, scopes)}
301 | }`
302 | case 'VariableDeclaration':
303 | var output = ''
304 | node.declarations.forEach((decl,i) => {
305 | output += ast_node_to_js(decl, scopes);
306 | if (i !== node.declarations.length-1) {
307 | output += ',\n'
308 | } else {
309 | output += ';'
310 | }
311 | });
312 | return output;
313 | case 'IfStatement':
314 | if (node.loc.start.line <= current_line_number && node.loc.end.line >= current_line_number) {
315 | // find which branch the cursor is in
316 | var test_node = node;
317 | while (test_node.type == 'IfStatement') {
318 | if (test_node.consequent.loc.start.line <= current_line_number && test_node.consequent.loc.end.line >= current_line_number) {
319 | break
320 | }
321 | test_node = test_node.alternate;
322 | }
323 | return `// if ${ast_node_to_js(test_node.test, scopes)}
324 | ${ast_node_to_js(test_node.consequent.body, scopes)}
325 | `
326 | }
327 | var else_clause = '';
328 | if (node.alternate) {
329 | else_clause = `else ${ast_node_to_js(node.alternate, scopes)}`
330 | }
331 |
332 | return `if (${ast_node_to_js(node.test, scopes)}) ${ast_node_to_js(node.consequent, scopes)} ${else_clause}`;
333 | case 'ConditionalExpression':
334 | return `${ast_node_to_js(node.test, scopes)} ? ${ast_node_to_js(node.consequent, scopes)} : ${ast_node_to_js(node.alternate, scopes)}`
335 | case 'AssignmentExpression':
336 | return `${ast_node_to_js(node.left, scopes)} ${node.operator} ${ast_node_to_js(node.right, scopes)}`;
337 | case 'MemberExpression':
338 | if (node.computed) {
339 | return `${ast_node_to_js(node.object, scopes)}[${ast_node_to_js(node.property, scopes)}]`
340 | } else {
341 | return `${ast_node_to_js(node.object, scopes)}.${ast_node_to_js(node.property, scopes)}`
342 | }
343 | case 'VariableDeclarator':
344 | if (variable_overrides[current_line_number] && node.id.name in variable_overrides[current_line_number]) {
345 | return `var ${node.id.name} = ${variable_overrides[current_line_number][node.id.name]}`;
346 | }
347 | return `var ${node.id.name} = ${ast_node_to_js(node.init, scopes)}`;
348 | case 'NewExpression':
349 | var arguments_str = '';
350 | node.arguments.forEach((arg,i) => {
351 | arguments_str += ast_node_to_js(arg, scopes)+ (i == node.arguments.length-1 ? '' : ', ');
352 | })
353 | return `new ${ast_node_to_js(node.callee, scopes)}(${arguments_str})`;
354 | case 'CallExpression':
355 | var arguments_str = '';
356 | node.arguments.forEach((arg,i) => {
357 | arguments_str += ast_node_to_js(arg, scopes)+ (i == node.arguments.length-1 ? '' : ', ');
358 | })
359 | return `${ast_node_to_js(node.callee, scopes)}(${arguments_str})`;
360 | case 'FunctionExpression':
361 | if (node.loc.start.line <= current_line_number && node.loc.end.line >= current_line_number) {
362 | return `{
363 | ${node.params.map(param => `var ${param.name} = ${variable_overrides[current_line_number] ? variable_overrides[current_line_number][param.name] : 'null'};`).join('\n')}
364 | ${ast_node_to_js(node.body, scopes)}
365 | }`
366 | } else {
367 | var params_str = '';
368 | node.params.forEach((param,i) => {
369 | params_str += ast_node_to_js(param, scopes)+ (i == node.params.length-1 ? '' : ', ');
370 | })
371 | return `${node.async ? 'async ':''} function(${params_str}) ${ast_node_to_js(node.body, scopes)}`;
372 | }
373 | case 'FunctionDeclaration':
374 | if (node.loc.start.line <= current_line_number && node.loc.end.line >= current_line_number) {
375 | return `{ // function ${ast_node_to_js(node.id, scopes)}
376 | ${node.params.map(param => `var ${param.name} = null;`).join('\n')}
377 | ${ast_node_to_js(node.body, scopes)}
378 | }`
379 | } else {
380 | var params_str = '';
381 | node.params.forEach((param,i) => {
382 | params_str += ast_node_to_js(param, scopes)+ (i == node.params.length-1 ? '' : ', ');
383 | })
384 | return `${node.async ? 'async ':''}function ${ast_node_to_js(node.id, scopes)}(${params_str}) ${ast_node_to_js(node.body, scopes)}`;
385 | }
386 | case 'ArrowFunctionExpression':
387 | return `(${node.params.map(param => ast_node_to_js(param, scopes)).join(', ')}) => ${ast_node_to_js(node.body, scopes)}`
388 | case 'ForStatement':
389 | return `for (${ast_node_to_js(node.init, scopes)} ${ast_node_to_js(node.test, scopes)}; ${ast_node_to_js(node.update, scopes)}) ${ast_node_to_js(node.body, scopes)}`
390 | case 'ReturnStatement':
391 | return `return ${node.argument ? ast_node_to_js(node.argument, scopes) : ''};`
392 | case 'UnaryExpression':
393 | return `${node.operator} ${ast_node_to_js(node.argument, scopes)}`
394 | case 'BinaryExpression':
395 | case 'LogicalExpression':
396 | return `${ast_node_to_js(node.left, scopes)} ${node.operator} ${ast_node_to_js(node.right, scopes)}`;
397 | case 'UpdateExpression':
398 | if (node.prefix) {
399 | return `${node.operator}${ast_node_to_js(node.argument, scopes)}`;
400 | }
401 | return `${ast_node_to_js(node.argument, scopes)}${node.operator}`;
402 | case 'TryStatement':
403 | return `try ${ast_node_to_js(node.block, scopes)} ${ast_node_to_js(node.handler, scopes)}`
404 | case 'CatchClause':
405 | return `catch(${node.param.name || ''}) ${ast_node_to_js(node.body, scopes)}`
406 | case 'ArrayExpression':
407 | var output = '';
408 | node.elements.forEach(element => {
409 | output += ast_node_to_js(element, scopes);
410 | if (i !== node.declarations.length-1) {
411 | output += ' ,\n'
412 | }
413 | })
414 | return `[${output}]`
415 | case 'ObjectExpression':
416 | return `{${node.properties.map(prop => ast_node_to_js(prop, scopes))}}`
417 | case 'Property':
418 | return `${ast_node_to_js(node.key, scopes)}: ${ast_node_to_js(node.value, scopes)}`
419 | case 'Identifier':
420 | return node.name;
421 | case 'ThisExpression':
422 | return 'this';
423 | case 'TemplateLiteral':
424 | return '`'+ ast_node_to_js(node.quasis, scopes) + '`'
425 | case 'TemplateElement':
426 | return node.value.raw;
427 | case 'Literal':
428 | return node.raw;
429 | case 'EmptyStatement':
430 | return ''
431 | default:
432 | console.log(node)
433 | return '!!!UH OH!!!'
434 | }
435 | }
436 |
--------------------------------------------------------------------------------
/new_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
299 |
927 |
928 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "replugger",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "electron_app.js",
6 | "dependencies": {
7 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
8 | "canvas": "^1.6.11",
9 | "circular-json": "^0.4.0",
10 | "codemirror": "^5.31.0",
11 | "deepcopy": "^0.6.3",
12 | "diff": "^3.5.0",
13 | "electron": "^1.7.9",
14 | "electron-prompt": "^1.1.0-1",
15 | "filbert": "^0.1.20",
16 | "flow-parser": "^0.58.0",
17 | "flow-remove-types": "^1.2.3",
18 | "jquery": "^3.2.1",
19 | "less": "^3.8.1",
20 | "mousetrap": "^1.6.2",
21 | "react": "^16.1.1",
22 | "react-dom": "^16.1.1",
23 | "underscore": "^1.8.3",
24 | "window": "^4.2.5"
25 | },
26 | "devDependencies": {},
27 | "scripts": {
28 | "start": "electron .",
29 | "test": "echo \"Error: no test specified\" && exit 1"
30 | },
31 | "author": "Glen Chiacchieri",
32 | "license": "ISC"
33 | }
34 |
--------------------------------------------------------------------------------
/splash_video.txt:
--------------------------------------------------------------------------------
1 | SPLASH VIDEO:
2 | - In this video I'm going to demonstrate a research prototype, a live code editor that mixes features of a REPL and a debugger, naturally called REPLugger. But first, a little context.
3 |
4 | - In the modern programming experience, most programmers type text into a text editor and imagine how the program works in their heads. They have to reconstruct the program's behavior mentally without much feedback from the program itself. This doesn't make any sense and leads to some problems.
5 |
6 | - The lack of feedback makes programming hard for beginners to understand, and this tragically limits the type of people that can access programming. The lack of feedback also leads to the creation of easily avoidable software bugs, and makes programming an unpleasant experience even for experts.
7 |
8 | - So instead of "playing computer" in our heads, how can we get the real computer in front of us to show us directly what our programs are doing?
9 |
10 | - We have some hints.
11 |
12 | - In a previous research prototype I made called Flowsheets, users type python code and see the results of that code immediately in a grid, kind of like a spreadsheet. This combination of code, data, and immediate feedback made programming in Flowsheets feel great, and Flowsheets became my preferred way to manipulate data even though it was just a research prototype. But the experience was geared more toward smaller scripts and not large programs. I began to wonder if there was a way of getting some of the benefits of Flowsheets in more complex software projects. So I created another prototype to test some of these ideas, which I'll show you now.
13 |
14 | There are basically three ways programmers get feedback about how their programs work today:
15 |
16 | 1. they can run the program using print statements
17 | 2. they can run the program with a debugger
18 | 3. and they can use a REPL or notebook environment
19 |
20 | these methods have some serious drawbacks
21 |
22 | 1. print statements aren't great because a programmer has to beg for information about their programs, which is like peeking through a tiny pinhole, especially for larger programs.
23 |
24 | 2. debuggers show more information about the state of the program at a particular place in the code, but stepping through code is slow and cumbersome, and the feedback loop between modifying the program and seeing the effects of those changes is long
25 |
26 | 3. And finally, REPLs and notebooks are fast and flexible, allowing programmers to try things out quickly, but there's a limit on the complexity of those experiments and it's hard to fold the results of experiments back into larger programs
27 |
28 | - But there's a fourth option that we're just beginning to explore: we can incorporate the results of running programs directly into the programming environment. I made a research environment called Flowsheets that runs python code and shows the results in a grid, kind of like a spreadsheet. Using Flowsheets it felt great to program and get immediate feedback about what my program was doing, but the experience was geared more toward smaller scripts and not large programs.
29 |
30 | - So, is there a way to bring some of the benefits of an environment like Flowsheets to larger programs without the drawbacks of print statements, debuggers, or REPLs?
31 |
32 | ------
33 |
34 | - This is REPLugger, an editor that integrates the liveliness of a REPL and the comprehensiveness of a debugger directly into the programming experience
35 |
36 | - on the left we have an editor. In this case we're editing a real file from a project with a few thousand lines of code
37 |
38 | - on the right we have a debugging interface, a table listing scopes, names, and values from a sample run of the program
39 |
40 | - and at the bottom we show the output of the line the cursor is on
41 |
42 | - changing the cursor changes the debugger and output UIs. You can see as I move the cursor, the names under the cursor are added to this table, and the output at the bottom changes too. And the same applies if I type new code. [[ type 1+1*3 ]]
43 |
44 | - why is this useful? here's an example from my experience. there's a function I often forget the order of the arguments to. In REPLugger, the values are shown and I can examine them directly [[ click down arrow in table ]],
45 |
46 | or I can simply type the function into the editor and see its definition right there, no looking up documentation. This feature is a standard part of many IDEs, but in REPLugger inline documentation comes for free.
47 |
48 | - The live output isn't just for documentation though, it's also useful in the ways a REPL is useful - I can write little expressions and see what happens. For example, in this program there's a grid and I want to see that grid's height. I can multiply the number of rows in the grid by the height of each row to see the total height of the grid [[ rows * cell_height ]]. I can change values this expression depends on and see how it affects the expression's output.
49 |
50 | - REPLugger tries to be smart about evaluating code. For example, say I wanted to create a condition if the grid height is bigger than the window's height. I can write this if-statement [[ if (height > window.innerHeight) { ]], and even though this isn't a valid line to type in a REPL by itself, REPLugger evaluates just the condition of this line. In this instance, we see the condition is true, but if I wanted more information I could highlight just one part of the expression and see that value of that part [[ window.innerHeight ]], letting me answer questions about why this code works the way it does directly in the editor.
51 |
52 | - Even with these simple features, REPLugger gives programmers useful concrete feedback about their programs. This could help beginners understand how their program is being interpreted, and help experts sharpen their mental models.
53 |
54 | - Unlike a REPL or notebook, expressions in REPLugger are evaluated within a larger program's context, closer to the conditions the actual program is running under. Let's look at that idea more closely.
55 |
56 | ---------
57 |
58 | The basic idea of REPLugger is to create a sample environment that gives us just enough feedback to be useful as we're programming.
59 |
60 | For example, when we move the cursor around [[ move in to clamp() function ]], REPLugger tries to execute the program up to the current line, but here it needs some context. These three names shown in red are arguments to this function. REPLugger doesn't know what the values of those arguments could be, so it asks you to fill them in. Let's do that [[ 10, 9, 11 ]]
61 |
62 | The values turn yellow to show they've been modified.
63 |
64 |
65 | Once they're filled in, the function's behavior can be explored. [[ cursor around ]]
66 |
67 | The arguments can be changed further to test out specific scenarios [[ null ]].
68 |
69 | And they're saved in the editor, so they can be reused when returning to the code later.
70 |
71 |
72 | ----------
73 |
74 |
75 | Filling in values is especially useful in more complex nested code [[ move to line 145 ]]. For example, on this line, we see a couple things. First, we see this line is in a function that needs an argument to be filled. Let's do that: I'll paste in a little code here: [[ new window.MouseEvent('mousemove'); evt.pageX = 0; evt.pageY = 0; ]] REPLugger evaluates that code and shows the result. I can click back on the value and REPLugger will show me the original expression.
76 |
77 | Next we see an error on line 139. What happened is REPLugger ignored this if-statement in an attempt to run up until the current line, but it encountered an error. As we can see from this table, resize_drag is null. This code expects resize_drag to be non-null. So what we can do is introduce an overwrite in this scope. I'll add a new name from this dropdown, and then enter a short expression. [[ new Resize_Drag(create_ui_block()) ]]
78 |
79 | What this does is create a new entry in the table that overwrites any previous values, but only in the context of this particular scope. If I navigate out of this scope, the overwrite is no longer active. Overwriting is useful in creating specific system states so that certain parts of code can run. In this case, we now have enough context to modify this part of the code, and we'll have live feedback as we do it. We can see all the expressions that relied on resize_drag now have sample values.
80 |
81 | These overwrites can be saved and used later, too. I'll save this one and name it "default". If I change this value to something else, I can get back to the scenario I named default by going to this dropdown menu and clicking it.
82 |
83 | You can imagine overwrites being useful for teams of programmers. If the overwrites were saved in source control, then any team member using REPLugger could use overwrites other programmers created to get live feedback as they program. Shared overwrites would also have the added benefit of giving programmers unfamiliar with a piece of code some context about what states the original programmer expected the code to run under.
84 |
85 | -------
86 |
87 |
88 |
89 | Finally, REPLugger also supports overwrites as a way of quickly and fluidly creating and validating experiments. Earlier, we tried changing a variable called cell_height directly in the code, but say we wanted to experiment with that value while we're in a completely different place in the code and to see what happens.
90 |
91 | What we can do is we can edit the value directly in this table. The value will be overwritten temporarily and anything that depends on that value will be updated accordingly. You can see the height variable changes as I change cell_height. These experiments can be saved as shown before.
92 |
93 | A lot of what makes programming hard, especially in big systems, is that there are so many different states a program can have. REPLugger gives programmers the ability to quickly modify these states directly and see what happens. Intuition is one of a programmer's best friends, but intuition works best given feedback and a low barrier to experimentation.
94 |
95 | ---------
96 |
97 | So that's REPLugger. I think it represents a scalable live programming environment that could be useful for both beginners and experts. In particular, I think the introduction of actual program data into the programming experience grounds understanding, making it so programmers don't have to imagine what the program is doing in their heads.
98 |
99 | As I said, REPLugger is a research prototype, which means I built it to demonstrate the idea. It's not a production-ready system, and would take a lot of engineering to get to a usable state. Unfortunately, I don't have the time to do this, but I think it would be worth the effort if someone did. If you're excited about turning REPLugger into a full-fledged product, I'd love to talk with you.
100 |
101 | And if you're interested in the future of programming, you might be interested in my newsletter where I send out very occasional emails about my projects. There's a link to it in the description. Thanks for watching!
102 |
103 | --------
104 |
105 | feedback from Michael Nagle:
106 | - overwrites felt powerful
107 | - the "working on team" statement felt provocative, but Michael didn't understand that the overwrites were saved
108 | - also didn't understand that the (evt) statement got evaluated. could show that more clearly, and that expressions are evaluated and can be edited later
109 | - programming felt so much calmer, instead of having code and then meta-code with print statements
110 |
111 | Geoffrey feedback:
112 | - surprised about amount of functionality
113 | - since program output was visual, wondering if he could somehow fold that into REPLugger
114 |
115 |
116 | --------
117 |
118 | thoughts:
119 | - working with teams: imagine having scenarios saved for other people working on code. Another programmer could use these examples to browse the code and see what it does concretely, and an idea of which conditions the original programmer expected the code to run in
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | - explain how running model works
128 | - basic idea is to get some kind of environment up and running, enough to be able to usefully play with things
129 | - tries to run the program until the line the cursor is on, but needs some context
130 | - for clamp, wants to call the function, but needs you to specify arguments
131 | - once inputted, arguments are saved for future use
132 | - especially useful for nested (show more complicated example, line 140)
133 |
134 | - setup context for function
135 | - line 140, error, to execute this line the environment ignored the if-statement
136 | - so resize_drag is null, want to populate it, new Resize_Drag(create_ui_block())
137 | - ability to save context, if these are saved in source control could imagine useful in collaborative code projects
138 |
139 | - finally, what-if situations
140 | - change name in any scope to see effects
141 | - quickly try out different values
142 | - earlier, we tried changing cell_height directly, but here we're editing code elsewhere - instead of going to find that value, we can just edit it directly, overwriting its value temporarily to experiment and see how the program changes (if more parts of the program depended on cell_height we'd see all those values change too)
143 | - can save these overwrites as before to be used again later if desired
144 | - imagine turning these into unit tests, but really more important are human factors
145 | - experimentation and intuition are programmers' best friends, but those capacities work better with more data. today, tools don't support fluid intuition and experimentation
146 |
147 | - speculative: use program, save state, see in REPLugger
148 |
149 | - important features:
150 | - instant feedback - does the thing work as I expect?
151 | - shows how model works - good for beginners and experts
152 | - what-if situations allow and encourage quickly trying out alternate states
153 |
154 | - future directions:
155 | - hacked together, better model
156 | - collaborative coding
157 | - save
--------------------------------------------------------------------------------
/staged_src/.babelrc:
--------------------------------------------------------------------------------
1 | //
2 | {"plugins": ["transform-flow-strip-types"]}
3 |
--------------------------------------------------------------------------------
/staged_src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flowsheets v2
6 |
7 |
8 |
9 |
10 |
11 |
12 |
32 |
33 |
34 |
35 |
36 |
').html('Visualization ▶').on('mouseenter', function () {
367 | $block.find('.submenu').remove(); // remove old ones if they're still around
368 |
369 | var $submenu = $('
').css({
370 | left: $menu.width() + 7,
371 | top: 30
372 | });
373 | $block.append($submenu);
374 |
375 | _.each(visualizations, function (react_component, name) {
376 | var text = name;
377 | var $li = $('