diff --git a/lib/builder.dart b/lib/builder.dart index f3e1ab8..222b2a0 100644 --- a/lib/builder.dart +++ b/lib/builder.dart @@ -237,6 +237,7 @@ class BuiltinStub { 'SchemeSymbol': 'symbol', 'Procedure': 'procedure', 'Pair': 'pair', + 'SchemeList': 'list', 'SchemeEventListener': 'event listener', 'JsValue': 'js object', 'JsProcedure': 'js function', diff --git a/lib/src/core/interpreter.dart b/lib/src/core/interpreter.dart index 02c7e47..174b238 100644 --- a/lib/src/core/interpreter.dart +++ b/lib/src/core/interpreter.dart @@ -34,6 +34,18 @@ class Interpreter { StandardLibrary().importAll(globalEnv); } + /// Creates a clone of this interpreter, with a copy of this interpreter's + /// current global environment and settings, but a reset logger, exit + /// listener, and frame counter. + Interpreter clone() { + var cloned = Interpreter(impl); + cloned.language = language; + cloned.tailCallOptimized = tailCallOptimized; + cloned.globalEnv.bindings.addAll(globalEnv.bindings); + cloned.globalEnv.hidden.addAll(globalEnv.hidden); + return cloned; + } + @deprecated ProjectInterface get implementation => impl; diff --git a/lib/src/core/standard_library.dart b/lib/src/core/standard_library.dart index 2cc0f67..07a94c1 100644 --- a/lib/src/core/standard_library.dart +++ b/lib/src/core/standard_library.dart @@ -244,13 +244,13 @@ class StandardLibrary extends SchemeLibrary with _$StandardLibraryMixin { bool isZero(Number x) => x == Number.zero; /// Forces [promise], evaluating it if necessary. - Expression force(Promise promise) => promise.force(); + Value force(Promise promise) => promise.force(); /// Finds the rest of [stream]. /// /// Equivalent to (force (cdr [stream])) @SchemeSymbol("cdr-stream") - Expression cdrStream(Pair stream) => force(cdr(stream)); + Value cdrStream(Pair stream) => force(cdr(stream)); /// Mutates the car of [pair] to be [val]. @SchemeSymbol("set-car!") diff --git a/lib/src/core/standard_library.g.dart b/lib/src/core/standard_library.g.dart index 821c21a..5ed8518 100644 --- a/lib/src/core/standard_library.g.dart +++ b/lib/src/core/standard_library.g.dart @@ -53,8 +53,8 @@ abstract class _$StandardLibraryMixin { bool isEven(Number x); bool isOdd(Number x); bool isZero(Number x); - Expression force(Promise promise); - Expression cdrStream(Pair stream); + Value force(Promise promise); + Value cdrStream(Pair stream); void setCar(Pair pair, Value val); void setCdr(Pair pair, Value val, Frame env); String getRuntimeType(Expression expression); @@ -65,7 +65,7 @@ abstract class _$StandardLibraryMixin { return this.apply(__exprs[0], SchemeList(__exprs[1]), __env); }, 2, docs: Docs("apply", "Applies [procedure] to the given [args]\n", - [Param("procedure", "procedure"), Param(null, "args")], + [Param("procedure", "procedure"), Param("list", "args")], returnType: "value")); addBuiltin(__env, const SchemeSymbol("display"), (__exprs, __env) { this.display(__exprs[0], __env); @@ -220,7 +220,8 @@ abstract class _$StandardLibraryMixin { }, 0, maxArgs: -1, docs: Docs.variable( - "list", "Constructs a list from zero or more arguments.\n")); + "list", "Constructs a list from zero or more arguments.\n", + returnType: "list")); addBuiltin(__env, const SchemeSymbol("map"), (__exprs, __env) { if (__exprs[0] is! Procedure || __exprs[1] is! PairOrEmpty) throw SchemeException('Argument of invalid type passed to map.'); @@ -229,7 +230,8 @@ abstract class _$StandardLibraryMixin { docs: Docs( "map", "Constructs a new list from calling [fn] on each item in [lst].\n", - [Param("procedure", "fn"), Param(null, "lst")])); + [Param("procedure", "fn"), Param("list", "lst")], + returnType: "list")); addBuiltin(__env, const SchemeSymbol("filter"), (__exprs, __env) { if (__exprs[0] is! Procedure || __exprs[1] is! PairOrEmpty) throw SchemeException('Argument of invalid type passed to filter.'); @@ -238,7 +240,8 @@ abstract class _$StandardLibraryMixin { docs: Docs( "filter", "Constructs a new list of all items in [lst] that return true when passed\nto [pred].\n", - [Param("procedure", "pred"), Param(null, "lst")])); + [Param("procedure", "pred"), Param("list", "lst")], + returnType: "list")); addBuiltin(__env, const SchemeSymbol("reduce"), (__exprs, __env) { if (__exprs[0] is! Procedure || __exprs[1] is! PairOrEmpty) throw SchemeException('Argument of invalid type passed to reduce.'); @@ -247,7 +250,7 @@ abstract class _$StandardLibraryMixin { docs: Docs( "reduce", "Reduces [lst] into a single expression by combining items with [combiner].\n", - [Param("procedure", "combiner"), Param(null, "lst")], + [Param("procedure", "combiner"), Param("list", "lst")], returnType: "value")); addVariableBuiltin(__env, const SchemeSymbol("+"), (__exprs, __env) { if (__exprs.any((x) => x is! Number)) @@ -421,7 +424,7 @@ abstract class _$StandardLibraryMixin { }, 1, docs: Docs("force", "Forces [promise], evaluating it if necessary.\n", [Param(null, "promise")], - returnType: "expression")); + returnType: "value")); addBuiltin(__env, const SchemeSymbol("cdr-stream"), (__exprs, __env) { if (__exprs[0] is! Pair) throw SchemeException('Argument of invalid type passed to cdr-stream.'); @@ -431,7 +434,7 @@ abstract class _$StandardLibraryMixin { "cdr-stream", "Finds the rest of [stream].\n\nEquivalent to (force (cdr [stream]))\n", [Param("pair", "stream")], - returnType: "expression")); + returnType: "value")); addBuiltin(__env, const SchemeSymbol("set-car!"), (__exprs, __env) { if (__exprs[0] is! Pair) throw SchemeException('Argument of invalid type passed to set-car!.'); diff --git a/lib/src/core/values.dart b/lib/src/core/values.dart index 6c7a464..d42a580 100644 --- a/lib/src/core/values.dart +++ b/lib/src/core/values.dart @@ -99,19 +99,19 @@ class Thunk extends Value { /// whose cdr is a promise that evaluates to another stream or the empty list. /// /// It is semantically different from a JS Promise, which is equivalent to a -/// Dart [Future]. The Scheme equivalent of [Future] is [AsyncExpression]. +/// Dart [Future]. The Scheme equivalent of [Future] is [AsyncValue]. /// /// A Scheme stream is semantically different from a Dart Stream, which is an /// asynchronous sequence. A Scheme stream is analogous to a lazily-computed /// iterable built on a linked list. class Promise extends Value { - Expression expr; + Value expr; final Frame env; bool _evaluated = false; Promise(this.expr, this.env); /// Evaluates the promise, or returns the result if already evaluated. - Expression force() { + Value force() { if (!_evaluated) { expr = schemeEval(expr, env); env.interpreter.language.validateCdr(expr, diff --git a/lib/src/extra/extra_library.g.dart b/lib/src/extra/extra_library.g.dart index be0492c..4e12e16 100644 --- a/lib/src/extra/extra_library.g.dart +++ b/lib/src/extra/extra_library.g.dart @@ -61,10 +61,9 @@ abstract class _$ExtraLibraryMixin { addBuiltin(__env, const SchemeSymbol("bindings"), (__exprs, __env) { return (this.bindings(__env)).list; }, 0, - docs: Docs( - "bindings", - "Returns a list of all bindings in the current environment.\n", - [])); + docs: Docs("bindings", + "Returns a list of all bindings in the current environment.\n", [], + returnType: "list")); addVariableBuiltin(__env, const SchemeSymbol('trigger-event'), (__exprs, __env) { this.triggerEvent(__exprs, __env); diff --git a/lib/src/extra/visualization.dart b/lib/src/extra/visualization.dart index d4dbcf0..760a0c8 100644 --- a/lib/src/extra/visualization.dart +++ b/lib/src/extra/visualization.dart @@ -11,12 +11,6 @@ class Button extends Widget { void Function() click; Widget inside; Button(this.inside, this.click); - Button.forEvent( - this.inside, SchemeSymbol id, List data, Frame env) { - click = () { - env.interpreter.triggerEvent(id, data, env); - }; - } } class Visualization extends Widget { @@ -29,6 +23,8 @@ class Visualization extends Widget { List buttonRow; Value result; + Function(int index, [bool keepAnimating]) goto; + Visualization(this.code, this.env) { Interpreter inter = env.interpreter; @@ -61,7 +57,7 @@ class Visualization extends Widget { _init() { bool animating = false; - goto(int index, [bool keepAnimating = false]) { + goto = (index, [keepAnimating = false]) { if (!keepAnimating) animating = false; if (index < 0) index = diagrams.length - 1; if (index >= diagrams.length - 1) { @@ -71,7 +67,7 @@ class Visualization extends Widget { current = index; buttonRow[2] = TextWidget("${current + 1}/${diagrams.length}"); update(); - } + }; Button first = Button(TextWidget("<<"), () => goto(0)); Button prev = Button(TextWidget("<"), () => goto(current - 1)); @@ -83,6 +79,7 @@ class Visualization extends Widget { animating = false; return; } + if (current == diagrams.length - 1) goto(0); animating = true; await Future.delayed(Duration(seconds: 1)); while (animating && current < diagrams.length - 1) { @@ -93,7 +90,7 @@ class Visualization extends Widget { buttonRow = [first, prev, status, next, last, animate]; } - void _addFrames(Frame myEnv, [Expression returnValue]) { + void _addFrames(Frame myEnv, [Value returnValue]) { if (myEnv.tag == '#imported') return; if (frameReturnValues.containsKey(myEnv)) { frameReturnValues[myEnv] = returnValue; diff --git a/lib/src/web/turtle_library.dart b/lib/src/web/turtle_library.dart index 303f9e0..ff94d82 100644 --- a/lib/src/web/turtle_library.dart +++ b/lib/src/web/turtle_library.dart @@ -50,7 +50,7 @@ class TurtleLibrary extends SchemeLibrary with _$TurtleLibraryMixin { @turtlestart @MinArgs(1) @MaxArgs(2) - void circle(List exprs) { + void circle(List exprs) { if (exprs[0] is! Number) { throw SchemeException('${exprs[0]} is not a number'); } @@ -96,7 +96,7 @@ class TurtleLibrary extends SchemeLibrary with _$TurtleLibraryMixin { /// Sets the pen color of the turtle. @turtlestart - void color(Expression color) { + void color(Value color) { turtle.penColor = Color.fromAnything(color); } @@ -123,13 +123,13 @@ class TurtleLibrary extends SchemeLibrary with _$TurtleLibraryMixin { /// Sets the background color of the turtle canvas. @turtlestart - void bgcolor(Expression color) { + void bgcolor(Value color) { turtle.backgroundColor = Color.fromAnything(color); } /// Sets the [size] of the turtle's pen. @turtlestart - void pensize(num size) { + void pensize(int size) { turtle.penSize = size; } @@ -169,7 +169,7 @@ class TurtleLibrary extends SchemeLibrary with _$TurtleLibraryMixin { /// Draws a box with [color] in the turtle's current pixel size at ([x], [y]) @turtlestart - void pixel(num x, num y, Expression color) { + void pixel(num x, num y, Value color) { turtle.drawPixel(x, y, Color.fromAnything(color)); } @@ -188,7 +188,6 @@ class TurtleLibrary extends SchemeLibrary with _$TurtleLibraryMixin { @SchemeSymbol('screen-height') num screenHeight() => turtle.gridHeight / turtle.pixelSize; - /// This turtle procedure is not supported in the web interpreter. @SchemeSymbol('unsupported') @SchemeSymbol('speed') @SchemeSymbol('showturtle') diff --git a/lib/src/web/turtle_library.g.dart b/lib/src/web/turtle_library.g.dart index aa7087a..f3a4ee8 100644 --- a/lib/src/web/turtle_library.g.dart +++ b/lib/src/web/turtle_library.g.dart @@ -9,23 +9,23 @@ abstract class _$TurtleLibraryMixin { void backward(num distance); void left(num angle); void right(num angle); - void circle(List exprs); + void circle(List exprs); void setPosition(num x, num y); void setHeading(num heading); void penUp(); void penDown(); void turtleClear(); - void color(Expression color); + void color(Value color); void beginFill(); void endFill(); void exitonclick(Frame env); void exit(); - void bgcolor(Expression color); - void pensize(num size); + void bgcolor(Value color); + void pensize(int size); void turtleHelp(Frame env); void setGridSize(int width, int height); void setCanvasSize(int width, int height); - void pixel(num x, num y, Expression color); + void pixel(num x, num y, Value color); void pixelsize(int size); num screenWidth(); num screenHeight(); @@ -90,10 +90,8 @@ abstract class _$TurtleLibraryMixin { __env.bindings[const SchemeSymbol('right')]; __env.hidden[const SchemeSymbol('rt')] = true; addVariableBuiltin(__env, const SchemeSymbol("circle"), (__exprs, __env) { - if (__exprs.any((x) => x is! Expression)) - throw SchemeException('Argument of invalid type passed to circle.'); turtle.show(); - this.circle(__exprs.cast()); + this.circle(__exprs); return undefined; }, 1, maxArgs: 2, @@ -153,14 +151,12 @@ abstract class _$TurtleLibraryMixin { return undefined; }, 0, docs: Docs('turtle-clear', "Clears the current turtle state.\n", [])); addBuiltin(__env, const SchemeSymbol("color"), (__exprs, __env) { - if (__exprs[0] is! Expression) - throw SchemeException('Argument of invalid type passed to color.'); turtle.show(); this.color(__exprs[0]); return undefined; }, 1, docs: Docs("color", "Sets the pen color of the turtle.\n", - [Param("expression", "color")])); + [Param("value", "color")])); addBuiltin(__env, const SchemeSymbol('begin_fill'), (__exprs, __env) { turtle.show(); this.beginFill(); @@ -194,8 +190,6 @@ abstract class _$TurtleLibraryMixin { docs: Docs('turtle-exit', "Closes the turtle canvas, reseting its state.\n", [])); addBuiltin(__env, const SchemeSymbol("bgcolor"), (__exprs, __env) { - if (__exprs[0] is! Expression) - throw SchemeException('Argument of invalid type passed to bgcolor.'); turtle.show(); this.bgcolor(__exprs[0]); return undefined; @@ -203,16 +197,16 @@ abstract class _$TurtleLibraryMixin { docs: Docs( "bgcolor", "Sets the background color of the turtle canvas.\n", - [Param("expression", "color")])); + [Param("value", "color")])); addBuiltin(__env, const SchemeSymbol("pensize"), (__exprs, __env) { - if (__exprs[0] is! Number) + if (__exprs[0] is! Integer) throw SchemeException('Argument of invalid type passed to pensize.'); turtle.show(); - this.pensize(__exprs[0].toJS()); + this.pensize(__exprs[0].toJS().toInt()); return undefined; }, 1, docs: Docs("pensize", "Sets the [size] of the turtle's pen.\n", - [Param("num", "size")])); + [Param("int", "size")])); addBuiltin(__env, const SchemeSymbol('turtle-help'), (__exprs, __env) { this.turtleHelp(__env); return undefined; @@ -244,9 +238,7 @@ abstract class _$TurtleLibraryMixin { "Sets the exterior dimensions of the turtle's canvas.\n\nThis does not effect the current state of the turtle.\n", [Param("int", "width"), Param("int", "height")])); addBuiltin(__env, const SchemeSymbol("pixel"), (__exprs, __env) { - if (__exprs[0] is! Number || - __exprs[1] is! Number || - __exprs[2] is! Expression) + if (__exprs[0] is! Number || __exprs[1] is! Number) throw SchemeException('Argument of invalid type passed to pixel.'); turtle.show(); this.pixel(__exprs[0].toJS(), __exprs[1].toJS(), __exprs[2]); @@ -255,11 +247,7 @@ abstract class _$TurtleLibraryMixin { docs: Docs( "pixel", "Draws a box with [color] in the turtle's current pixel size at ([x], [y])\n", - [ - Param("num", "x"), - Param("num", "y"), - Param("expression", "color") - ])); + [Param("num", "x"), Param("num", "y"), Param("value", "color")])); addBuiltin(__env, const SchemeSymbol("pixelsize"), (__exprs, __env) { if (__exprs[0] is! Integer) throw SchemeException('Argument of invalid type passed to pixelsize.'); @@ -297,10 +285,7 @@ abstract class _$TurtleLibraryMixin { 'Argument of invalid type passed to unsupported.'); this.unsupported(__exprs.cast(), __env); return undefined; - }, 0, - maxArgs: -1, - docs: Docs.variable('unsupported', - "This turtle procedure is not supported in the web interpreter.\n")); + }, 0, maxArgs: -1); __env.bindings[const SchemeSymbol('speed')] = __env.bindings[const SchemeSymbol('unsupported')]; __env.hidden[const SchemeSymbol('speed')] = true; diff --git a/lib/src/web/web_library.dart b/lib/src/web/web_library.dart index 57dcbcf..4fe8a3a 100644 --- a/lib/src/web/web_library.dart +++ b/lib/src/web/web_library.dart @@ -19,8 +19,9 @@ class WebLibrary extends SchemeLibrary with _$WebLibraryMixin { final JsObject jsPlumb; final String css; final html.Element styleElement; + final Function startEditor; - WebLibrary(this.jsPlumb, this.css, this.styleElement) { + WebLibrary(this.jsPlumb, this.css, this.styleElement, this.startEditor) { Undefined.jsUndefined = context['undefined']; AsyncValue.makePromise = (expr) => JsObject(context['Promise'], [ (resolve, reject) { @@ -171,15 +172,26 @@ class WebLibrary extends SchemeLibrary with _$WebLibraryMixin { /// Loads and applies a [theme]. Future theme(SchemeSymbol theme, Frame env) async { ImportedLibrary lib = await import('scm/theme/$theme', [], env); - Value myTheme = lib.reference(const SchemeSymbol('imported-theme')); - if (myTheme is! Theme) throw SchemeException("No theme exists"); - applyThemeBuiltin(myTheme); + // For old-style themes + try { + applyThemeBuiltin( + lib.reference(const SchemeSymbol('imported-theme')) as Theme); + } on SchemeException catch (e) { + // Ignore + } return undefined; } /// Converts [color] to a string of CSS. @SchemeSymbol("color->css") String colorToCss(Color color) => color.toCSS(); + + /// Launch the editor. + /// + /// Note: This is still a work in progress. Don't use for important work! + void editor(Frame env) { + startEditor(env.interpreter.clone()); + } } StreamController _controller = StreamController(); diff --git a/lib/src/web/web_library.g.dart b/lib/src/web/web_library.g.dart index 67456be..c46c102 100644 --- a/lib/src/web/web_library.g.dart +++ b/lib/src/web/web_library.g.dart @@ -25,6 +25,7 @@ abstract class _$WebLibraryMixin { Value libraryReference(ImportedLibrary imported, SchemeSymbol id); Future theme(SchemeSymbol theme, Frame env); String colorToCss(Color color); + void editor(Frame env); void importAll(Frame __env) { addVariableBuiltin(__env, const SchemeSymbol("js"), (__exprs, __env) { return this.js(__exprs); @@ -206,5 +207,13 @@ abstract class _$WebLibraryMixin { docs: Docs("color->css", "Converts [color] to a string of CSS.\n", [Param("color", "color")], returnType: "string")); + addBuiltin(__env, const SchemeSymbol("editor"), (__exprs, __env) { + this.editor(__env); + return undefined; + }, 0, + docs: Docs( + "editor", + "Launch the editor.\n\nNote: This is still a work in progress. Don't use for important work!\n", + [])); } } diff --git a/lib/src/web_ui/code_input.dart b/lib/src/web_ui/code_input.dart index 6486ad1..c818cd4 100644 --- a/lib/src/web_ui/code_input.dart +++ b/lib/src/web_ui/code_input.dart @@ -35,18 +35,17 @@ class CodeInput { element = SpanElement() ..classes = ['code-input'] ..contentEditable = 'true'; - _autoBox = DivElement() - ..classes = ["docs"] - ..style.visibility = "hidden"; + _autoBox = DivElement()..style.visibility = "hidden"; _autoBoxWrapper = DivElement() ..classes = ["render"] ..append(_autoBox); _subs.add(element.onKeyPress.listen(_onInputKeyPress)); _subs.add(element.onKeyDown.listen(_keyListener)); _subs.add(element.onKeyUp.listen(_keyListener)); + _subs.add(document.onSelectionChange.listen(_selectListener)); log.append(element); log.append(_autoBoxWrapper); - element.focus(); + if (runCode != null) element.focus(); parenListener ??= (_) => null; parenListener(missingParens); } @@ -87,7 +86,15 @@ class CodeInput { } } - Future _onInputKeyPress(KeyboardEvent event) async { + Future _onInputKeyPress(KeyboardEvent event) { + if (runCode != null) { + return _replKeyPress(event); + } else { + return _editorKeyPress(event); + } + } + + Future _replKeyPress(KeyboardEvent event) async { if ((missingParens ?? -1) > 0 && event.shiftKey && event.keyCode == KeyCode.ENTER) { @@ -121,6 +128,32 @@ class CodeInput { parenListener(missingParens); } + Future _editorKeyPress(KeyboardEvent event) async { + if ((missingParens ?? -1) > 0 && + event.shiftKey && + event.keyCode == KeyCode.ENTER) { + event.preventDefault(); + element.text = element.text.trimRight() + ')' * missingParens + '\n\n'; + await highlight(atEnd: true); + } else if (KeyCode.ENTER == event.keyCode) { + event.preventDefault(); + int cursor = findPosition(element, window.getSelection().getRangeAt(0)); + String newInput = element.text; + String first = newInput.substring(0, cursor) + "\n"; + String second = ""; + if (cursor != newInput.length) { + second = newInput.substring(cursor); + } + int spaces = _countSpace(newInput, cursor); + element.text = first + " " * spaces + second; + await highlight(cursor: cursor + spaces + 1); + } else { + await delay(5); + await highlight(saveCursor: true); + } + parenListener(missingParens); + } + /// Determines the operation at the last open parens. /// /// Returns a two item list where the first item indicates the word that was matched @@ -225,6 +258,7 @@ class CodeInput { void _autocomplete() { // Find the text to the left of where the typing cursor currently is. int cursorPos = findPosition(element, window.getSelection().getRangeAt(0)); + if (cursorPos > element.text.length) cursorPos = element.text.length; List matchingWords = []; int currLength = 0; // Find the last word that being typed [output] or the second to last operation that was typed [output2]. @@ -245,20 +279,28 @@ class CodeInput { } // Clear whatever is currently in the box. _autoBox.children = []; - _autoBox.classes = ["docs"]; + _autoBox.classes = []; if (matchingWords.isEmpty) { - // If there are no matching words, hide the autocomplete box. + // If there are no matching words, hide the autocomplete box in the REPL tabComplete = ""; _autoBox.style.visibility = "hidden"; + // In the editor, list a full word as a current match anyway. + if (runCode == null && match.isNotEmpty && isFullWord) { + _autoBox.classes = ["docs"]; + _autoBox.append(SpanElement() + ..classes = ["autobox-word"] + ..innerHtml = "($match ...)"); + _autoBox.style.visibility = "visible"; + } } else if (matchingWords.length == 1) { // If there is only one matching word, display the docs for that word. render(wordToDocs[matchingWords.first], _autoBox); - _autoBox.style.visibility = "hidden"; - _autoBox.children.last.style.visibility = "visible"; + _autoBox.style.visibility = "visible"; tabComplete = matchingWords.first.substring(currLength); - } else { + } else if (matchingWords.isNotEmpty) { tabComplete = ""; // Add each matching word as its own element for formatting purposes. + _autoBox.classes = ["docs"]; for (String match in matchingWords) { _autoBox.append(SpanElement() ..classes = ["autobox-word"] @@ -268,7 +310,9 @@ class CodeInput { } _autoBox.style.visibility = "visible"; } - _autoBox.scrollIntoView(); + if (runCode != null) { + _autoBox.scrollIntoView(); + } } _keyListener(KeyboardEvent event) async { @@ -291,4 +335,8 @@ class CodeInput { await highlight(cursor: cursor + tabComplete.length + 1); } } + + _selectListener(event) { + if (_isAutocompleteEnabled) _autocomplete(); + } } diff --git a/lib/src/web_ui/editor.dart b/lib/src/web_ui/editor.dart new file mode 100644 index 0000000..887b10b --- /dev/null +++ b/lib/src/web_ui/editor.dart @@ -0,0 +1,492 @@ +library web_ui.editor; + +import 'dart:convert' show json; +import 'dart:html'; +import 'dart:math'; + +import 'package:cs61a_scheme/cs61a_scheme_web.dart'; + +import 'code_input.dart'; +import 'repl.dart'; + +/// A tabbed editor. +class Editor { + /// The element that this editor is contained in. + Element container; + + /// The sidebar element for this editor. + Element sidebar; + + /// The tab element for this editor. + Element tabs; + + /// The element that contains the tabs and visible buffer. + Element content; + + /// The list of buffers open in this editor. + List buffers; + + /// This editor's global interpreter. + Interpreter interpreter; + + Map docs; + + /// The index of the tab that is currently active. + int activeTab; + + /// Height of the drawer when visible. + int drawerHeight = 350; + + /// Width of the sidebar when visible. + int sidebarWidth = 300; + + /// Loads an editor attached to [interpreter] into [container]. + Editor(this.interpreter, this.container) { + docs = allDocumentedForms(interpreter.globalEnv); + buffers = []; + sidebar = DivElement()..classes = ['sidebar']; + var toggle = DivElement()..classes = ['sidebar-toggle']; + var sidebarAdjust = DivElement()..classes = ['sidebar-adjust']; + toggle.onClick.listen((e) { + if (sidebar.classes.contains('collapsed')) { + sidebar.classes.remove('collapsed'); + } else { + sidebar.classes.add('collapsed'); + } + saveState(); + }); + container.append(toggle); + container.append(sidebar); + sidebarAdjust.onMouseDown.listen((e) { + var oldWidth = sidebarWidth; + if (sidebar.classes.contains('collapsed')) { + sidebar.style.width = '0'; + sidebar.classes.remove('collapsed'); + } + var listeners = [ + container.onMouseMove.listen((e) { + sidebarWidth = e.client.x - toggle.clientWidth; + sidebar.style.width = '${sidebarWidth}px'; + }) + ]; + var cancelAll = (e) { + for (var listener in listeners) { + listener.cancel(); + } + if (sidebarWidth < 20) { + sidebarWidth = oldWidth; + sidebar.style.width = '${sidebarWidth}px'; + sidebar.classes.add('collapsed'); + } + saveState(); + }; + listeners.add(container.onMouseUp.listen(cancelAll)); + listeners.add(container.onMouseLeave.listen(cancelAll)); + }); + container.append(sidebarAdjust); + content = DivElement()..classes = ['content']; + tabs = DivElement()..classes = ['tabs']; + content.append(tabs); + tabs.append(DivElement() + ..classes = ['new-tab'] + ..text = '+' + ..onClick.listen((event) { + newTab(); + })); + container.append(content); + setupSidebar(); + setupKeyboardShortcuts(); + if (window.localStorage.containsKey('#editor-state')) { + restoreState(json.decode(window.localStorage['#editor-state'])); + } else { + newTab(); + } + } + + /// Opens a new tab with [text]. + /// + /// This will become the active tab if [active] is true. + newTab({String text = "", bool active = true}) { + var buffer = Buffer.text(text, this)..attachTab(tabs); + buffer.input.enableAutocomplete(); + buffers.add(buffer); + if (active) replaceBuffer(buffer); + buffer.input.parenListener = (_) => saveState(); + } + + /// Closes [buffer]. + /// + /// If this was the last buffer in the editor, closes the editor. + closeTab(Buffer buffer) { + bool active = buffer.tab.classes.contains('tab-active'); + int index = buffers.indexOf(buffer); + buffers.remove(buffer); + buffer.tab.remove(); + if (buffers.isEmpty) { + closeEditor(); + } else if (active) { + replaceBuffer(buffers[max(index - 1, 0)]); + } + saveState(); + } + + /// Closes this editor. + closeEditor() { + container.remove(); + } + + /// Makes [buffer] the active tab. + replaceBuffer(Buffer buffer) { + for (var child in tabs.children) { + child.classes.remove('tab-active'); + } + buffer.tab.classes.add('tab-active'); + if (content.children.length > 1) { + content.lastChild.remove(); + } + content.append(buffer.element); + activeTab = buffers.indexOf(buffer); + buffer.input.highlight(saveCursor: true); + saveState(); + } + + /// Saves the state of the editor. + saveState() { + if (buffers.isEmpty) { + window.localStorage.remove('#editor-state'); + return; + } + var state = { + 'sidebar-collapsed': sidebar.classes.contains('collapsed'), + 'tabs': buffers.map((b) => b.serialize()).toList(), + 'activeTab': activeTab, + 'drawerHeight': drawerHeight, + 'sidebarWidth': sidebarWidth, + }; + window.localStorage['#editor-state'] = json.encode(state); + } + + /// Restores a previously saved editor state. + restoreState(Map state) { + if (state['sidebar-collapsed']) { + sidebar.classes.add('collapsed'); + } + for (var tab in state['tabs']) { + buffers.add(Buffer.deserialize(tab, this)..attachTab(tabs)); + } + for (var buffer in buffers) { + buffer.input.parenListener = (_) => saveState(); + } + drawerHeight = state['drawerHeight'] ?? 350; + sidebarWidth = state['sidebarWidth'] ?? 300; + sidebar.style.width = '${sidebarWidth}px'; + replaceBuffer(buffers[state['activeTab']]); + } + + /// Sets up the sidebar with sections for user files and themes and sample + /// files and themes. + setupSidebar() { + sidebar.append(DivElement() + ..classes = ['sidebar-header'] + ..text = 'My Files'); + var myFiles = DivElement()..classes = ['sidebar-section']; + sidebar.append(myFiles); + sidebar.append(DivElement() + ..classes = ['sidebar-header'] + ..text = 'My Themes'); + var myThemes = DivElement()..classes = ['sidebar-section']; + sidebar.append(myThemes); + sidebar.append(DivElement() + ..classes = ['sidebar-header'] + ..text = 'Sample Apps'); + var sampleApps = DivElement()..classes = ['sidebar-section']; + addSampleApp(sampleApps, "chess"); + addSampleApp(sampleApps, "drawing"); + sidebar.append(sampleApps); + sidebar.append(DivElement() + ..classes = ['sidebar-header'] + ..text = 'Sample Themes'); + var sampleThemes = DivElement()..classes = ['sidebar-section']; + addSampleTheme(sampleThemes, "default"); + addSampleTheme(sampleThemes, "solarized"); + addSampleTheme(sampleThemes, "monochrome"); + addSampleTheme(sampleThemes, "monochrome-dark"); + addSampleTheme(sampleThemes, "go-bears"); + sidebar.append(sampleThemes); + } + + /// Adds an entry to the sidebar section [container]. + addEntry(Element container, String text, Function onClick) { + container.append(DivElement() + ..classes = ['sidebar-entry'] + ..text = text + ..onClick.listen((e) => onClick())); + } + + /// Adds a sample app to the sidebar. + addSampleApp(Element container, String id) { + addEntry(container, '$id.scm', () async { + var text = await HttpRequest.getString('scm/apps/$id.scm'); + newTab(text: text); + }); + } + + /// Adds a sample theme to the sidebar. + addSampleTheme(Element container, String id) { + addEntry(container, '$id.scm', () async { + var text = await HttpRequest.getString('scm/theme/$id.scm'); + newTab(text: text); + }); + } + + /// Listens for keyboard shortcuts and adds links to the sidebar. + setupKeyboardShortcuts() { + altW() => closeTab(buffers[activeTab]); + altT() => newTab(); + altR() => buffers[activeTab].run(); + altV() => buffers[activeTab].visualize(); + ctrlS() => buffers[activeTab].save(); + shiftTab(int amount) { + activeTab += amount; + if (activeTab >= buffers.length) activeTab = 0; + if (activeTab < 0) activeTab = buffers.length - 1; + replaceBuffer(buffers[activeTab]); + } + + container.onKeyDown.listen((e) { + print('${e.keyCode} ${e.key} ${e.ctrlKey} ${e.shiftKey} ${e.altKey}'); + if (e.altKey) { + if (e.keyCode == KeyCode.W) { + e.preventDefault(); + closeTab(buffers[activeTab]); + } else if (e.keyCode == KeyCode.T) { + e.preventDefault(); + newTab(); + } else if (e.keyCode == KeyCode.R) { + e.preventDefault(); + buffers[activeTab].run(); + } else if (e.keyCode == KeyCode.V) { + e.preventDefault(); + buffers[activeTab].visualize(); + } + } else if (e.ctrlKey) { + if (e.keyCode == KeyCode.S) { + e.preventDefault(); + ctrlS(); + } else if (e.keyCode == KeyCode.APOSTROPHE) { + e.preventDefault(); + shiftTab(e.shiftKey ? -1 : 1); + } + } + }); + + sidebar.append(DivElement() + ..classes = ['sidebar-header'] + ..text = 'Keyboard Shortcuts'); + var shortcuts = DivElement()..classes = ['sidebar-section']; + addEntry(shortcuts, "Close Tab (Alt-W)", altW); + addEntry(shortcuts, "New Tab (Alt-T)", altT); + addEntry(shortcuts, "Save (Ctrl-S)", ctrlS); + addEntry(shortcuts, "Next Tab (Ctrl-`)", () => shiftTab(1)); + addEntry(shortcuts, "Previous Tab (Ctrl-Shift-`)", () => shiftTab(-1)); + addEntry(shortcuts, "Run Code (Alt-R)", altR); + addEntry(shortcuts, "Visualize (Alt-V)", altV); + sidebar.append(shortcuts); + } +} + +/// An editor window built on [CodeInput]. +class Buffer { + /// The editor this buffer is attached to. + final Editor editor; + + /// The DOM element for this buffer. + Element element; + + /// The input field for this buffer. + CodeInput input; + + /// The DOM element for this buffer's drawer. + Element drawer; + + /// The DOM element for the handle to adjust the height of the drawer. + Element drawerAdjust; + + /// The DOM element this buffer's tab (if any) is stored in. + Element tab; + + /// The title of this buffer if any. + String title; + + /// Creates a new buffer with [text] attached to [editor]. + Buffer.text(String text, this.editor) { + element = DivElement()..classes = ['buffer']; + var inputElement = DivElement()..classes = ['input-wrapper']; + element.append(inputElement); + input = CodeInput(inputElement, null, editor.docs); + input.text = text; + drawerAdjust = DivElement()..classes = ['drawer-adjust']; + element.append(drawerAdjust); + drawer = DivElement()..classes = ['drawer']; + element.append(drawer); + var buttonContainer = DivElement()..classes = ['buttons']; + setupButtons(buttonContainer); + element.append(buttonContainer); + } + + /// Deserializes a previously serialized buffer. + Buffer.deserialize(Map serialized, this.editor) { + element = DivElement()..classes = ['buffer']; + var inputElement = DivElement()..classes = ['input-wrapper']; + element.append(inputElement); + input = CodeInput(inputElement, null, editor.docs); + input.text = serialized['text']; + title = serialized['title']; + drawerAdjust = DivElement()..classes = ['drawer-adjust']; + element.append(drawerAdjust); + drawer = DivElement()..classes = ['drawer']; + element.append(drawer); + var buttonContainer = DivElement()..classes = ['buttons']; + setupButtons(buttonContainer); + element.append(buttonContainer); + } + + /// Constructs a tab that's associated with this buffer. + attachTab(Element tabContainer) { + tab = Element.div()..classes = ['tab']; + var tabTitle = Element.span()..text = title ?? 'Untitled'; + var tabClose = Element.div() + ..classes = ['tab-close'] + ..text = '×'; + /*var tabIndicator = Element.div() + ..classes = ['tab-indicator'] + ..text = ' ';*/ + tab.append(tabTitle); + tab.append(tabClose); + //tab.append(tabIndicator); + tab.onClick.listen((e) { + if (e.target != tabClose) editor.replaceBuffer(this); + }); + tabClose.onClick.listen((e) { + editor.closeTab(this); + }); + tabContainer.append(tab); + } + + /// Sets up the buttons at the bottom of the buffer. + setupButtons(Element buttonContainer) { + buttonContainer.append(AnchorElement() + ..classes = ['button'] + ..text = 'Save' + ..onClick.listen(save)); + runButton = AnchorElement() + ..classes = ['button'] + ..text = 'Run' + ..onClick.listen(run); + buttonContainer.append(runButton); + vizButton = AnchorElement() + ..classes = ['button'] + ..text = 'Visualize' + ..onClick.listen(visualize); + buttonContainer.append(vizButton); + drawerAdjust.onMouseDown.listen((e) { + var listeners = [ + element.onMouseMove.listen((e) { + editor.drawerHeight = element.clientHeight - e.client.y; + drawer.style.height = '${editor.drawerHeight}px'; + }) + ]; + var cancelAll = (e) { + for (var listener in listeners) { + listener.cancel(); + } + editor.saveState(); + }; + listeners.add(element.onMouseUp.listen(cancelAll)); + listeners.add(element.onMouseLeave.listen(cancelAll)); + }); + } + + /// Saves this buffer. Not yet implemented. + save([_]) { + // TODO(jathak): Implement files + window.alert("Your work is automatically saved. " + "Saving to a file is not yet supported."); + } + + /// Opens a REPL in the drawer and runs the code currently in this buffer. + /// + /// If a REPL is already open, this instead closes the drawer. + run([_]) { + if (isReplOpen) { + closeDrawer(); + } else { + openDrawer(); + runButton.text = 'Close'; + isReplOpen = true; + var repl = Repl(editor.interpreter.clone(), drawer); + repl.interpreter.onExit = closeDrawer; + repl.runCode(input.text, fromTool: true); + repl.activeInput.highlight(); + } + } + + /// Runs the visualizer and displays the result in the drawer. + /// + /// If the visualization is already open, this instead closes the drawer. + visualize([_]) { + if (isVizOpen) { + closeDrawer(); + } else { + openDrawer(); + vizButton.text = 'Close'; + isVizOpen = true; + var renderBox = DivElement(); + drawer.append(renderBox); + var interpreter = editor.interpreter.clone(); + var code = []; + var tokens = tokenizeLines(input.text.split('\n')).toList(); + while (tokens.isNotEmpty) { + code.add(schemeRead(tokens, interpreter)); + } + var viz = Visualization(code, interpreter.globalEnv); + viz.goto(viz.diagrams.length - 1); + render(viz, renderBox); + } + } + + Element runButton, vizButton; + + bool isReplOpen = false, isVizOpen = false; + + /// Opens the drawer. + openDrawer() async { + runButton.text = 'Run'; + vizButton.text = 'Visualize'; + isReplOpen = false; + isVizOpen = false; + drawer.children.clear(); + drawer.style.height = '${editor.drawerHeight}px'; + drawerAdjust.style.display = 'block'; + await delay(100); + drawerAdjust.style.display = 'block'; + } + + /// Closes the drawer. + /// + /// This also hides the turtle canvas if it's open. + closeDrawer() async { + runButton.text = 'Run'; + vizButton.text = 'Visualize'; + isReplOpen = false; + isVizOpen = false; + drawer.style.height = '0'; + drawer.children.clear(); + await delay(100); + drawerAdjust.style.display = 'none'; + querySelector('#turtle').style.display = 'none'; + } + + /// Serializes this buffer. + serialize() => {'title': title, 'text': input.text}; +} diff --git a/lib/src/web_ui/repl.dart b/lib/src/web_ui/repl.dart index 64204bb..221e978 100644 --- a/lib/src/web_ui/repl.dart +++ b/lib/src/web_ui/repl.dart @@ -27,10 +27,13 @@ class Repl { if (decoded is List) history = decoded.map((item) => '$item').toList(); } addBuiltins(); - container = PreElement()..classes = ['repl']; + container = DivElement()..classes = ['repl']; container.onClick.listen((e) async { - if (!activeInput.element.contains(document.activeElement)) { + await delay(100); + if (window.getSelection().rangeCount == 0 || + window.getSelection().getRangeAt(0).collapsed) { activeInput.element.focus(); + await activeInput.highlight(atEnd: true); } }); parent.append(container); @@ -57,7 +60,7 @@ class Repl { print('Stack Trace: ${e.stackTrace}'); } }; - window.onKeyDown.listen(onWindowKeyDown); + container.onKeyDown.listen(onKeyDownListener); } bool autodraw = false; @@ -114,9 +117,11 @@ class Repl { container.scrollTop = container.scrollHeight; } - runCode(String code) async { - addToHistory(code); - buildNewInput(); + runCode(String code, {bool fromTool: false}) async { + if (!fromTool) { + addToHistory(code); + buildNewInput(); + } var tokens = tokenizeLines(code.split("\n")).toList(); var loggingArea = activeLoggingArea; while (tokens.isNotEmpty) { @@ -178,7 +183,7 @@ class Repl { } } - onWindowKeyDown(KeyboardEvent event) { + onKeyDownListener(KeyboardEvent event) { if (activeInput.text.trim().contains('\n') && !event.ctrlKey) return; if (event.keyCode == KeyCode.UP) { historyUp(); diff --git a/lib/styles/_diagram.scss b/lib/styles/_diagram.scss index 429c929..1ad70db 100644 --- a/lib/styles/_diagram.scss +++ b/lib/styles/_diagram.scss @@ -1,5 +1,23 @@ @import 'theme'; +.button { + font-size: 0.92em; + padding: 0.25em 0.375em; + border-radius: 0.1875em; + margin: 0.5em; + display: inline-block; + min-width: 2em; + text-align: center; + z-index: 250; + cursor: pointer; + user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + @include button; + @include button-text; +} + .render { @include render; @@ -17,24 +35,6 @@ } } - .button { - font-size: 0.92em; - padding: 0.25em 0.375em; - border-radius: 0.1875em; - margin: 0.5em; - display: inline-block; - min-width: 2em; - text-align: center; - z-index: 250; - cursor: pointer; - user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -webkit-user-select: none; - @include button; - @include button-text; - } - .block { display: inline-block; padding: 0.3em; @@ -103,8 +103,10 @@ margin: 0.5em; border-radius: 0.185em; width: 80ch; - font-size: 90%; + font-size: 85%; display: block; + white-space: pre-wrap; + word-wrap: break-word; @include other-frame; .comment { diff --git a/lib/styles/_editor.scss b/lib/styles/_editor.scss new file mode 100644 index 0000000..f60c69f --- /dev/null +++ b/lib/styles/_editor.scss @@ -0,0 +1,210 @@ +@import 'theme'; + +.editor { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + @include background; + + display: flex; + + .sidebar-toggle { + width: 16px; + @include other-frame; + opacity: 0.75; + } + + .sidebar { + width: 300px; + transition: width 0.08s linear; + @include render; + @include other-frame; + border: none; + overflow-x: hidden; + overflow-y: auto; + + &.collapsed { + width: 0 !important; + } + + + .sidebar-header { + padding: 16px 16px 0 16px; + font-family: sans-serif; + text-transform: uppercase; + } + + .sidebar-section { + padding: 16px; + + &:empty { + min-height: 40px; + } + + &:empty::after { + font-size: 75%; + content: "Nothing Here Yet!"; + } + } + + .sidebar-entry { + cursor: pointer; + } + } + + .sidebar-adjust { + height: 100%; + width: 3px; + background: currentcolor; + opacity: 0.8; + cursor: ew-resize; + } + + .content { + display: flex; + flex-direction: column; + flex: 1; + } + + .tabs { + display: flex; + $tab-height: 40px; + height: $tab-height; + min-height: $tab-height; + font-family: sans-serif; + cursor: pointer; + + @include render; + + .tab { + display: inline; + flex: 1; + text-align: center; + position: relative; + padding-top: 9px; + transition: background 0.08s linear; + @include other-frame; + border-color: transparent transparent currentcolor transparent; + &:hover:not(.tab-active) { + @include current-frame; + border-color: transparent transparent currentcolor transparent; + opacity: 0.8; + } + } + + .tab-active { + @include current-frame; + border-color: currentcolor currentcolor transparent currentcolor; + &:nth-child(2) { + border-left-color: transparent; + } + } + + .new-tab { + order: 1000; + padding: 7px 15px 0px 15px; + transition: background 0.08s linear; + @include other-frame; + border-color: transparent transparent currentcolor transparent; + &:hover { + @include current-frame; + border-color: transparent transparent currentcolor transparent; + opacity: 0.8; + } + } + + .tab-close, .tab-indicator { + position: absolute; + right: 0; + top: 0; + padding: 0px 5px 2px 5px; + margin: 9px; + border-radius: 10px; + transition: background 0.2s linear, color 0.2s linear; + } + + .tab-indicator { + height: 8px; + right: 3px; + margin-top: 13px; + background: #57f; + display: none; + } + } + + .buffer { + flex: 1; + display: flex; + flex-direction: column; + + .input-wrapper { + display: flex; + flex-direction: column; + flex: 1; + .code-input { + font-family: $font-stack; + flex: 1; + padding: 16px; + overflow-y: auto; + } + + .docs { + margin: 0; + border-radius: 0; + padding: 16px; + width: calc(100% - 32px); + } + + .render { + height: auto; + padding: 0; + } + } + + .drawer-adjust { + display: none; + width: 100%; + height: 3px; + background: currentcolor; + opacity: 0.8; + cursor: ns-resize; + } + + .drawer { + transition: height 0.08s linear; + position: relative; + overflow-y: auto; + + & > .render { + position: absolute; + max-height: 100%; + overflow-y: auto; + margin: 0; + left:0; + right:0; + bottom:0; + } + } + + .buttons { + display: flex; + flex-direction: row; + + .button { + flex: 1; + margin: 0; + border-radius: 0; + padding: 10px 0; + } + } + + .themes { + display: flex; + flex-direction: column; + width: 200px; + } + } + +} diff --git a/lib/styles/_input.scss b/lib/styles/_input.scss new file mode 100644 index 0000000..b5a2486 --- /dev/null +++ b/lib/styles/_input.scss @@ -0,0 +1,17 @@ +@import "theme"; + +.code-input { + display: inline-block; + white-space: pre-wrap; + word-wrap: break-word; + //overflow-y:auto; + + &:focus { + outline: none; + } +} + +.autobox-word { + margin: 10px; + display: inline-block; +} diff --git a/lib/styles/_repl.scss b/lib/styles/_repl.scss new file mode 100644 index 0000000..f8ea655 --- /dev/null +++ b/lib/styles/_repl.scss @@ -0,0 +1,74 @@ +@import "theme"; + +.repl { + padding: 16px; + margin: 0; + position:absolute; + left:0; + top:0; + right:0; + bottom:0; + + white-space: pre-wrap; + word-wrap: break-word; + overflow-y:auto; + + @include background; + @include text; + + a:not(.button){ + @include link; + } + + .repl-prompt { + vertical-align: top; + @include prompt; + } + + .repl-async { + @include async-log; + } + + .repl-status { + position: fixed; + right: 8px; + bottom: 8px; + font-size: 80%; + @include status; + } + + .render { + position: relative; + margin: 0; + padding: 0; + display: block; + } + + .render td { + margin: 0; + padding: 0; + } + + .error { + @include error; + } + + .mouseover-wrapper { + .render { + display: none; + } + .objects { + padding: 0; + } + &:hover { + .render { + z-index: 1000; + border-radius: 0.185em; + position: absolute; + width: auto; + display: block; + @include other-frame; + } + } + } +} diff --git a/lib/styles/_syntax.scss b/lib/styles/_syntax.scss new file mode 100644 index 0000000..3eaca16 --- /dev/null +++ b/lib/styles/_syntax.scss @@ -0,0 +1,17 @@ +@import "theme"; + +body { + font-family: $font-stack; + font-size: 0.92em; + @include text; +} + +.hljs-builtin-normal { @include spec-keyword; } +.hljs-builtin-special, .hljs-builtin-turtle { @include extra-keyword; } + +.hljs-number { @include number; } +.hljs-literal { @include boolean; } +.hljs-string { @include string; } +.hljs-name { @include procedure; } +.hljs-symbol { @include symbol; } +.hljs-comment { @include comment; } diff --git a/lib/styles/_theme.scss b/lib/styles/_theme.scss index d24c7ea..d42a6a1 100644 --- a/lib/styles/_theme.scss +++ b/lib/styles/_theme.scss @@ -1,4 +1,7 @@ -// This file is designed to be processed by the Scheme Interpreter's theme. +$font-stack: 'Inconsolata-g', 'Inconsolata', monospace; + +// The comments in these mixins designed to be processed by the +// Scheme Interpreter's theme. // /*!COLOR||*//*!END*/ // /*!CSS|*//*!END*/ diff --git a/lib/styles/_turtle.scss b/lib/styles/_turtle.scss new file mode 100644 index 0000000..d07afed --- /dev/null +++ b/lib/styles/_turtle.scss @@ -0,0 +1,26 @@ +#turtle{ + position: absolute; + right: 0; + top: 0; + width: 31.25em; + height: 31.25em; + z-index:100; + background:#fff; + display:none; + + @media all and (max-width: 768px) { + transform-origin: 100% 0% 0; + transform: scale(0.75,0.75); + -webkit-transform-origin: 100% 0% 0; + -webkit-transform: scale(0.75,0.75); + } + + @media all and (max-width: 500px) { + #turtle { + transform-origin: 100% 0% 0; + transform: scale(0.5,0.5); + -webkit-transform-origin: 100% 0% 0; + -webkit-transform: scale(0.5,0.5); + } + } +} diff --git a/lib/web_ui.dart b/lib/web_ui.dart index cd7607e..e480696 100644 --- a/lib/web_ui.dart +++ b/lib/web_ui.dart @@ -1 +1,2 @@ +export 'src/web_ui/editor.dart'; export 'src/web_ui/repl.dart'; diff --git a/web/assets/style.scss b/web/assets/style.scss index 92102dd..ee14de6 100644 --- a/web/assets/style.scss +++ b/web/assets/style.scss @@ -1,5 +1,9 @@ -@import 'package:cs61a_scheme/styles/theme'; +@import 'package:cs61a_scheme/styles/syntax'; +@import 'package:cs61a_scheme/styles/input'; @import 'package:cs61a_scheme/styles/diagram'; +@import 'package:cs61a_scheme/styles/editor'; +@import 'package:cs61a_scheme/styles/repl'; +@import 'package:cs61a_scheme/styles/turtle'; @font-face { font-family: 'Inconsolata-g'; @@ -8,137 +12,7 @@ font-style: normal; } -$font-stack: 'Inconsolata-g', 'Inconsolata', monospace; - - body { margin: 0; padding: 0; } - -#turtle{ - position: absolute; - right: 0; - top: 0; - width: 31.25em; - height: 31.25em; - z-index:100; - background:#fff; - display:none; - - @media all and (max-width: 768px) { - transform-origin: 100% 0% 0; - transform: scale(0.75,0.75); - -webkit-transform-origin: 100% 0% 0; - -webkit-transform: scale(0.75,0.75); - } - - @media all and (max-width: 500px) { - #turtle { - transform-origin: 100% 0% 0; - transform: scale(0.5,0.5); - -webkit-transform-origin: 100% 0% 0; - -webkit-transform: scale(0.5,0.5); - } - } -} - -.repl { - padding: 16px; - margin: 0; - position:absolute; - left:0; - top:0; - right:0; - bottom:0; - width: calc(100%-16px); - height: calc(100%-16px); - font-family: $font-stack; - font-size: 0.92em; - - white-space: pre-wrap; - word-wrap: break-word; - overflow-y:auto; - - @include background; - @include text; - - a:not(.button){ - @include link; - } - - .repl-prompt { - vertical-align: top; - @include prompt; - } - - .repl-async { - @include async-log; - } - - .repl-status { - position: fixed; - right: 8px; - bottom: 8px; - font-size: 80%; - @include status; - } - - .render { - position: relative; - margin: 0; - padding: 0; - display: block; - } - - .render td { - margin: 0; - padding: 0; - } - - .error { - @include error; - } - - .mouseover-wrapper { - .render { - display: none; - } - .objects { - padding: 0; - } - &:hover { - .render { - z-index: 1000; - border-radius: 0.185em; - position: absolute; - width: auto; - display: block; - @include other-frame; - } - } - } -} - -.autobox-word { - margin: 10px; - display: inline-block; -} - -.code-input { - display: inline-block; -} - -.code-input:focus { - outline: none; -} - -.hljs-builtin-normal { @include spec-keyword; } -.hljs-builtin-special, .hljs-builtin-turtle { @include extra-keyword; } - -.hljs-number { @include number; } -.hljs-literal { @include boolean; } -.hljs-string { @include string; } -.hljs-name { @include procedure; } -.hljs-symbol { @include symbol; } -.hljs-comment { @include comment; } diff --git a/web/main.dart b/web/main.dart index 175de39..7e74618 100644 --- a/web/main.dart +++ b/web/main.dart @@ -43,7 +43,7 @@ Shift+Enter to add missing parens and run the current input main() async { String css = await HttpRequest.getString('assets/style.css'); var style = querySelector('#theme'); - var webLibrary = WebLibrary(context['jsPlumb'], css, style); + var webLibrary = WebLibrary(context['jsPlumb'], css, style, startEditor); if (window.location.href.contains('logic')) { await startLogic(webLibrary); } else { @@ -67,6 +67,12 @@ main() async { }); } +startEditor(Interpreter interpreter) async { + var container = DivElement()..classes = ['editor']; + document.body.append(container); + var editor = Editor(interpreter, container); +} + startScheme(WebLibrary webLibrary) async { var inter = Interpreter(StaffProjectImplementation()); var normals = inter.globalEnv.bindings.keys.toSet(); @@ -106,6 +112,9 @@ startScheme(WebLibrary webLibrary) async { }) ]); inter.logger(MarkdownWidget(motd, env: demos), true); + if (window.location.toString().contains("editor")) { + startEditor(inter); + } } addDemo(Frame env, String demoName, String code) { diff --git a/web/scm/apps/chess.scm b/web/scm/apps/chess.scm index 40a9523..db32275 100644 --- a/web/scm/apps/chess.scm +++ b/web/scm/apps/chess.scm @@ -1,3 +1,5 @@ +#lang 61a-scheme/fa18 + (define (piece type color file rank) (list type color file rank)) (define (type piece) (car piece)) (define (piece-color piece) (car (cdr piece))) @@ -48,17 +50,17 @@ ((equal? t "bishop") (draw-bishop x y)) ((equal? t "pawn") (draw-pawn x y)) (else nil)))) - + (define (_draw-pieces pieces) (if (not (null? pieces)) (begin (draw-piece (car pieces)) (_draw-pieces (cdr pieces))))) - + (define (_redraw-square f r) (_fill-square f r (equal? current-highlight (cons f r))) (define piece-there (piece-at f r)) (if (null? piece-there) nil (draw-piece piece-there))) - + (define (_fill-square f r highlight) (goto (* (- f 5) 80) @@ -70,7 +72,7 @@ (color light-board-color))) (begin_fill) (seth 90) (forward 80) (left 90) (forward 80) (left 90) (forward 80) (left 90) (end_fill)) - + (define (_draw-highlighted) (_fill-square (car current-highlight) (cdr current-highlight) #t)) @@ -89,7 +91,7 @@ (pensize 0) (goto -320 320) (board 1)) - + (make-board) (_draw-highlighted) (_draw-pieces black-pieces) @@ -198,7 +200,7 @@ (helper (cdr lst))) (else #t))) (helper (if (equal? (piece-color king) "white") black white))) - + (define (_find-king pieces) (cond ((null? pieces) nil) ((equal? (type (car pieces)) "king") (car pieces)) @@ -238,12 +240,12 @@ (set! white-pieces old-white) (set! black-pieces old-black) (error-notrace "You may not move yourself into check"))) - + (set! current (if (equal? current "white") "black" "white")) (_redraw-square f1 r1) (_redraw-square f2 r2) (define king (_find-king (if (equal? current "white") white-pieces black-pieces))) - + (define (determine-check) (define in-check (_in-check king white-pieces black-pieces)) (define msg (if in-check (string-append (piece-color king) " king in check\n") nil)) diff --git a/web/scm/theme/default.scm b/web/scm/theme/default.scm index d693100..0c476b0 100644 --- a/web/scm/theme/default.scm +++ b/web/scm/theme/default.scm @@ -1 +1 @@ -(define imported-theme (make-theme)) +(apply-theme (make-theme)) diff --git a/web/scm/theme/go-bears.scm b/web/scm/theme/go-bears.scm index 4060602..4610737 100644 --- a/web/scm/theme/go-bears.scm +++ b/web/scm/theme/go-bears.scm @@ -1,3 +1,3 @@ (import 'scm/theme/monochrome 'make-monochrome-theme) -(define imported-theme (make-monochrome-theme (hex "#041e42") (hex "#ffc72c"))) +(apply-theme (make-monochrome-theme (hex "#041e42") (hex "#ffc72c"))) diff --git a/web/scm/theme/monochrome-dark.scm b/web/scm/theme/monochrome-dark.scm index 99b074b..feb7b40 100644 --- a/web/scm/theme/monochrome-dark.scm +++ b/web/scm/theme/monochrome-dark.scm @@ -1,3 +1,3 @@ (import 'scm/theme/monochrome 'make-monochrome-theme) -(define imported-theme (make-monochrome-theme (hex "#000") (hex "#fff"))) +(apply-theme (make-monochrome-theme (hex "#000") (hex "#fff"))) diff --git a/web/scm/theme/monochrome.scm b/web/scm/theme/monochrome.scm index 0ae0488..990c0f7 100644 --- a/web/scm/theme/monochrome.scm +++ b/web/scm/theme/monochrome.scm @@ -30,7 +30,7 @@ (theme-set-color! t 'button-text-hover background) (theme-set-color! t 'async-log foreground) (theme-set-css! t 'async-log "font-style: italic;") - + (theme-set-color! t 'background background) (theme-set-color! t 'current-frame background) (theme-set-css! t 'current-frame (string-append "border: 0.125em solid " (color->css foreground) ";")) @@ -44,9 +44,9 @@ (theme-set-css! t 'promise (string-append "border: 0.125em solid " (color->css foreground) ";")) (theme-set-color! t 'async-block background) (theme-set-css! t 'async-block (string-append "border: 0.125em solid " (color->css foreground) ";")) - + (theme-set-css! t 'button (string-append "border: 0.125em solid " (color->css foreground) ";")) (theme-set-css! t 'button-hover (string-append "border: 0.125em solid " (color->css foreground) ";")) t) -(define imported-theme (make-monochrome-theme (hex "#fff") (hex "#000"))) +(apply-theme (make-monochrome-theme (hex "#fff") (hex "#000"))) diff --git a/web/scm/theme/solarized.scm b/web/scm/theme/solarized.scm index 7d114b3..746f8a3 100644 --- a/web/scm/theme/solarized.scm +++ b/web/scm/theme/solarized.scm @@ -41,7 +41,7 @@ (theme-set-color! t 'button-text-hover base3) (theme-set-color! t 'async-log yellow) (theme-set-css! t 'async-log "font-style: italic;") - + (theme-set-color! t 'background base03) (theme-set-color! t 'current-frame blue) (theme-set-color! t 'other-frame base01) @@ -56,4 +56,4 @@ (theme-set-css! t 'async-block border) t) -(define imported-theme (make-solarized-theme)) +(apply-theme (make-solarized-theme))