Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Implementation of Autocomplete #77

Merged
merged 32 commits into from
Oct 22, 2018
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ecde679
Added autocomplete box that will display options
dnachum Aug 23, 2018
a902410
hide and display the autocomplete box
dnachum Aug 23, 2018
0266595
add the matching strings to the autocomplete box
dnachum Aug 23, 2018
523caf3
fixed bug when adding new lines
dnachum Aug 24, 2018
b95a844
added matching prefixes being bolded
dnachum Aug 24, 2018
5b8313f
Merge branch 'master' of https://github.com/Cal-CS-61A-Staff/dart_sch…
dnachum Aug 26, 2018
d1766ca
_wordMatches now compares all documented forms
dnachum Aug 26, 2018
f0245e0
fixed the width and made the height of the autocomplete box variable
dnachum Aug 26, 2018
c0d5ea5
fixed border around words in autobox
dnachum Aug 26, 2018
569f2ba
Make the autobox invisible when displaying docs
dnachum Aug 26, 2018
1f5f0fd
docs stay up after one space to handle cases like cons vs cons-stream
dnachum Aug 26, 2018
de7ac76
now looks at the last symbol next to an open parens to decide what do…
dnachum Sep 1, 2018
324e280
fixed autobox disappearing on empty parens
dnachum Sep 1, 2018
238618c
rewrote and cleaned up the _currOp function
dnachum Sep 3, 2018
54a499f
finds both the last op and the second to last op
dnachum Sep 3, 2018
0d426a9
changed the logic for determining if an operation represents a string…
dnachum Sep 5, 2018
a361b10
wrapped the autobox with a render element
dnachum Sep 5, 2018
86f01fd
removed the wrapper autBox instead of just the child nodes
dnachum Sep 9, 2018
7e50b67
allow autocomplete to be toggled on and off
dnachum Sep 9, 2018
40aab92
autocomplete toggle remembered in different sessions
dnachum Sep 9, 2018
f09f963
will complete word on tab if the docs for that word are displayed
dnachum Sep 9, 2018
2b66225
fixed spelling error with local storage of autocomplete preference
dnachum Sep 16, 2018
8ffb398
scroll the autobox into view if it expands below the current window
dnachum Sep 19, 2018
de1031b
made some syntax/naming fixes
dnachum Oct 17, 2018
126f612
cleared up stuff with local storage
dnachum Oct 17, 2018
68d95fd
cleaned up comment formatting and removed unnecesary code
dnachum Oct 17, 2018
d2f762d
Used Tuple2 instead of a generic List
dnachum Oct 17, 2018
a4fdd3a
changed some variable names
dnachum Oct 17, 2018
3fc75ba
fixed some comments and fixed bug when there exists a space after a o…
dnachum Oct 17, 2018
49e1f90
fixed some variable names and cleared up some comments
dnachum Oct 17, 2018
a21f8d3
added dependency and fixed an error with a space before the name of a…
dnachum Oct 17, 2018
c1e48d1
fixed boolean variable naming
dnachum Oct 22, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
148 changes: 141 additions & 7 deletions lib/src/web_ui/code_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ library web_ui.code_input;
import 'dart:async';
import 'dart:html';

import 'package:tuple/tuple.dart';

import 'package:cs61a_scheme/cs61a_scheme.dart';
import 'package:cs61a_scheme/cs61a_scheme_web.dart';

import 'highlight.dart';

Expand All @@ -14,22 +17,35 @@ const List<SchemeSymbol> noIndentForms = [
SchemeSymbol("define-macro")
];

bool _isEnabledAutocomplete = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: _isAutocompleteEnabled is probably a better name here


class CodeInput {
Element element;
Element _autoBox;
Element _autoBoxWrapper;
Map<String, Docs> wordToDocs;
bool _active = true;
final List<StreamSubscription> _subs = [];
String tabComplete = "";

Function(String code) runCode;
Function(int parens) parenListener;

CodeInput(Element log, this.runCode, {this.parenListener}) {
CodeInput(Element log, this.runCode, this.wordToDocs, {this.parenListener}) {
element = SpanElement()
..classes = ['code-input']
..contentEditable = 'true';
_autoBox = DivElement()
..classes = ["docs"]
..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));
log.append(element);
log.append(_autoBoxWrapper);
element.focus();
parenListener ??= (_) => null;
parenListener(missingParens);
Expand All @@ -49,11 +65,15 @@ class CodeInput {
void deactivate() {
_active = false;
element.contentEditable = 'false';
_autoBoxWrapper.remove();
for (var sub in _subs) {
sub.cancel();
}
}

void enableAutocomplete() => _isEnabledAutocomplete = true;
void disableAutocomplete() => _isEnabledAutocomplete = false;

Future highlight(
{bool saveCursor = false, int cursor, bool atEnd = false}) async {
if (saveCursor) {
Expand Down Expand Up @@ -101,17 +121,59 @@ class CodeInput {
parenListener(missingParens);
}

/// Determines how much space to indent the next line, based on parens
/// Determines the operation at the last open parens.
///
/// Returns a two item list where the first item indicates the word that was matched
/// and the second item indicates if that word is the full string (true)
/// or is in progress of being written out (false).
Tuple2<String, bool> _currOp(String inputText, int position,
[int fromLast = 1]) {
List<String> splitLines = inputText.substring(0, position).split("\n");
bool isMultipleLines = false;
String refLine;
int totalMissingCount = 0;

for (refLine in splitLines.reversed) {
totalMissingCount += countParens(refLine) ?? 0;
// Find the first line with an open paren but no close parentheses.
if (totalMissingCount >= fromLast) break;
isMultipleLines = true;
}
// If there are not enough open parentheses, return the default output value.
if (totalMissingCount >= fromLast) {
int strIndex = refLine.indexOf("(");
while (strIndex != -1 && strIndex < (refLine.length - 1)) {
int nextClose = refLine.indexOf(")", strIndex + 1);
int nextOpen = refLine.indexOf("(", strIndex + 1);
// Find the open parentheses that corresponds to the missing closed parentheses.
if (totalMissingCount > fromLast) {
totalMissingCount -= 1;
} else if (nextOpen == -1 || nextClose == -1 || nextOpen < nextClose) {
// Find the operations separated by any number of spaces, open parentheses, or closed parentheses.
List splitRef =
refLine.substring(strIndex + 1).split(RegExp("[ ()]+"));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this regular expression is actually doing. I recommend including a comment describing the meaning whenever you use all but the most trivial regexes, since they are notoriously difficult to understand.

String op =
splitRef.firstWhere((s) => s.isNotEmpty, orElse: () => "");
return Tuple2(op,
(splitRef.length - splitRef.indexOf(op)) > 1 || isMultipleLines);
}
strIndex = nextOpen;
}
}
return const Tuple2("", true);
}

/// Determines how much space to indent the next line, based on parens.
int _countSpace(String inputText, int position) {
List<String> splitLines = inputText.substring(0, position).split("\n");
// If the cursor is at the end of the line but not the end of the input, we
// must find that line and start counting parens from there
// must find that line and start counting parens from there.
String refLine;
int totalMissingCount = 0;
for (refLine in splitLines.reversed) {
// Truncate to position of cursor when in middle of the line
// Truncate to position of cursor when in middle of the line.
totalMissingCount += countParens(refLine);
// Find the first line with an open paren but no close paren
// Find the first line with an open paren but no close paren.
if (totalMissingCount >= 1) break;
}
if (totalMissingCount == 0) {
Expand All @@ -121,13 +183,13 @@ class CodeInput {
while (strIndex < (refLine.length - 1)) {
int nextClose = refLine.indexOf(")", strIndex + 1);
int nextOpen = refLine.indexOf("(", strIndex + 1);
// Find the open paren that corresponds to the missing closed paren
// Find the open paren that corresponds to the missing closed paren.
if (totalMissingCount > 1) {
totalMissingCount -= 1;
} else if (nextOpen == -1 || nextOpen < nextClose) {
Iterable<Expression> tokens = tokenizeLine(refLine.substring(strIndex));
Expression symbol = tokens.elementAt(1);
// Align subexpressions if any exist; otherwise, indent by two spaces
// Align subexpressions if any exist; otherwise, indent by two spaces.
if (symbol == const SchemeSymbol("(")) {
return strIndex + 1;
} else if (noIndentForms.contains(symbol)) {
Expand All @@ -145,8 +207,75 @@ class CodeInput {
return strIndex + 2;
}

/// Find the list of words that contain the same prefix as currWord.
List<String> _wordMatches(String currWord, bool fullWord) {
List<String> matchingWords = [];
for (String schemeWord in wordToDocs.keys) {
if (((schemeWord.length > currWord.length) && !fullWord) ||
schemeWord.length == currWord.length) {
if (schemeWord.substring(0, currWord.length) == currWord) {
matchingWords.add(schemeWord);
}
}
}
return matchingWords;
}

/// Finds and displays the possible words that the user may be typing.
void _autocomplete() {
// Find the text to the left of where the typing cursor currently is.
int cursorPos = findPosition(element, window.getSelection().getRangeAt(0));
List<String> matchingWords = [];
int currLength = 0;
// Find the last word that being typed [output] or the second to last operation that was typed [output2].
// Used for the case where an empty open parentheses is being typed.
// Eventually, may be used to implement different autocomplete behavior for special forms.
Tuple2<String, bool> output = _currOp(element.text, cursorPos);
Tuple2<String, bool> secondToLastOutput =
_currOp(element.text, cursorPos, 2);
String match = output.item1;
bool isFullWord = output.item2;
if (match.isEmpty) {
match = secondToLastOutput.item1;
isFullWord = secondToLastOutput.item2;
}
if (match.isNotEmpty) {
matchingWords = _wordMatches(match, isFullWord);
currLength = match.length;
}
// Clear whatever is currently in the box.
_autoBox.children = [];
_autoBox.classes = ["docs"];
if (matchingWords.isEmpty) {
// If there are no matching words, hide the autocomplete box.
tabComplete = "";
_autoBox.style.visibility = "hidden";
} 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";
tabComplete = matchingWords.first.substring(currLength);
} else {
tabComplete = "";
// Add each matching word as its own element for formatting purposes.
for (String match in matchingWords) {
_autoBox.append(SpanElement()
..classes = ["autobox-word"]
// Bold the matching characters.
..innerHtml =
"<strong>${match.substring(0, currLength)}</strong>${match.substring(currLength)}");
}
_autoBox.style.visibility = "visible";
}
_autoBox.scrollIntoView();
}

_keyListener(KeyboardEvent event) async {
int key = event.keyCode;
if (_isEnabledAutocomplete) {
_autocomplete();
}
if (key == KeyCode.BACKSPACE) {
await delay(0);
parenListener(missingParens);
Expand All @@ -155,6 +284,11 @@ class CodeInput {
await delay(0);
parenListener(missingParens);
await highlight(saveCursor: true);
} else if (key == KeyCode.TAB) {
event.preventDefault();
int cursor = findPosition(element, window.getSelection().getRangeAt(0));
element.appendText(tabComplete);
await highlight(cursor: cursor + tabComplete.length + 1);
}
}
}
25 changes: 22 additions & 3 deletions lib/src/web_ui/repl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class Repl {
int historyIndex = -1;

final String prompt;

Repl(this.interpreter, Element parent, {this.prompt = 'scm> '}) {
if (window.localStorage.containsKey('#repl-history')) {
var decoded = json.decode(window.localStorage['#repl-history']);
Expand All @@ -38,6 +37,12 @@ class Repl {
status = SpanElement()..classes = ['repl-status'];
container.append(status);
buildNewInput();
if (window.localStorage.containsKey('#autocomplete')) {
String val = window.localStorage['#autocomplete'];
if (val == 'enabled') {
activeInput.enableAutocomplete();
}
}
interpreter.logger = (logging, [newline = true]) {
var box = SpanElement();
activeLoggingArea.append(box);
Expand Down Expand Up @@ -79,6 +84,19 @@ class Repl {
autodraw = false;
return undefined;
}, 0);
addBuiltin(env, const SchemeSymbol('autocomplete'), (_a, _b) {
logText('While typing, will display a list of possible procedures.\n'
'(disable-autocomplete) to disable\n');
activeInput.enableAutocomplete();
window.localStorage['#autocomplete'] = 'enabled';
return undefined;
}, 0);
addBuiltin(env, const SchemeSymbol('disable-autocomplete'), (_a, _b) {
logText('Autocomplete disabled\n');
activeInput.disableAutocomplete();
window.localStorage['#autocomplete'] = 'disabled';
return undefined;
}, 0);
}

buildNewInput() {
Expand All @@ -90,8 +108,9 @@ class Repl {
..text = prompt
..classes = ['repl-prompt'];
container.append(activePrompt);
activeInput =
CodeInput(container, runCode, parenListener: updateParenStatus);
activeInput = CodeInput(
container, runCode, allDocumentedForms(interpreter.globalEnv),
parenListener: updateParenStatus);
container.scrollTop = container.scrollHeight;
}

Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
analyzer: ^0.32.4
markdown: ^2.0.2
quiver: ^2.0.0+1
tuple: ^1.0.2

dev_dependencies:
# Implementation library
Expand Down
5 changes: 5 additions & 0 deletions web/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ body {
}
}

.autobox-word {
margin: 10px;
display: inline-block;
}

.code-input {
display: inline-block;
}
Expand Down
2 changes: 2 additions & 0 deletions web/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const String motd = "**61A Scheme Web Interpreter 2.0.0-beta**"

**Other Useful Commands**
`(docs symbol)` to display documentation for some built-in or special form
`(autocomplete)` to display scheme built-ins and documentation while typing [Try It](:try-autocomplete)
`(clear)` to clear all output on the screen
`(theme 'id)` to change the interpreter's theme
[default](:try-default) [solarized](:try-solarized) """
Expand Down Expand Up @@ -81,6 +82,7 @@ startScheme(WebLibrary webLibrary) async {
addDemo(demos, 'try-draw', "(draw '(1 2 3))");
addDemo(demos, 'try-chess', "(import 'scm/apps/chess)");
addDemo(demos, 'try-ad', "(autodraw)");
addDemo(demos, 'try-autocomplete', "(autocomplete)");
addDemo(demos, 'try-default', "(theme 'default)");
addDemo(demos, 'try-solarized', "(theme 'solarized)");
addDemo(demos, 'try-monochrome', "(theme 'monochrome)");
Expand Down