-
Notifications
You must be signed in to change notification settings - Fork 5
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
Changes from 23 commits
ecde679
a902410
0266595
523caf3
b95a844
5b8313f
d1766ca
f0245e0
c0d5ea5
569f2ba
1f5f0fd
de7ac76
324e280
238618c
54a499f
0d426a9
a361b10
86f01fd
7e50b67
40aab92
f09f963
2b66225
8ffb398
de1031b
126f612
68d95fd
d2f762d
a4fdd3a
3fc75ba
49e1f90
a21f8d3
c1e48d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import 'dart:async'; | |
import 'dart:html'; | ||
|
||
import 'package:cs61a_scheme/cs61a_scheme.dart'; | ||
import 'package:cs61a_scheme/cs61a_scheme_web.dart'; | ||
|
||
import 'highlight.dart'; | ||
|
||
|
@@ -14,25 +15,40 @@ const List<SchemeSymbol> noIndentForms = [ | |
SchemeSymbol("define-macro") | ||
]; | ||
|
||
bool _enableAutocomplete = false; | ||
|
||
class CodeInput { | ||
Element element; | ||
//Create an element that contains possible autcomplete options | ||
Element _autoBox; | ||
Element _wrapperAutoBox; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Effective Dart advises that the most descriptive noun should generally go last in a variable name. I think |
||
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, Frame env, {this.parenListener}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to keep I'd recommend calling |
||
element = SpanElement() | ||
..classes = ['code-input'] | ||
..contentEditable = 'true'; | ||
_autoBox = DivElement() | ||
..classes = ["docs"] | ||
..style.visibility = "hidden"; | ||
_wrapperAutoBox = 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(_wrapperAutoBox); | ||
element.focus(); | ||
parenListener ??= (_) => null; | ||
parenListener(missingParens); | ||
wordToDocs = allDocumentedForms(env); | ||
} | ||
|
||
String get text => element.text; | ||
|
@@ -49,11 +65,15 @@ class CodeInput { | |
void deactivate() { | ||
_active = false; | ||
element.contentEditable = 'false'; | ||
_wrapperAutoBox.remove(); | ||
for (var sub in _subs) { | ||
sub.cancel(); | ||
} | ||
} | ||
|
||
void enableAutocomplete() => _enableAutocomplete = true; | ||
void disableAutocomplete() => _enableAutocomplete = false; | ||
|
||
Future highlight( | ||
{bool saveCursor = false, int cursor, bool atEnd = false}) async { | ||
if (saveCursor) { | ||
|
@@ -101,6 +121,43 @@ class CodeInput { | |
parenListener(missingParens); | ||
} | ||
|
||
/// Determines the operation at the last open parens | ||
List _currOp(String inputText, int position, [int fromLast = 1]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's usually best not to use a list to return multiple values of different types, since we lose type checking. I'm okay with it if we include a detailed description of the two values (including types) in the doc comment, but I think the better way to go would be to use something like the tuple package |
||
List<String> splitLines = inputText.substring(0, position).split("\n"); | ||
//The first item indicates the word that was matched, the second item indicates | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do first and second item refer to here? If it's the return values, this should be described in the doc comment above the function, not inline. |
||
//if that word is the full string(true) or is in progress of being written out(false) | ||
bool multipleLines = 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 paren | ||
if (totalMissingCount >= fromLast) break; | ||
multipleLines = 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 paren that corresponds to the missing closed paren | ||
if (totalMissingCount > fromLast) { | ||
totalMissingCount -= 1; | ||
} else if (nextOpen == -1 || nextClose == -1 || nextOpen < nextClose) { | ||
//Assuming the word is right after the parens | ||
List splitRef = | ||
refLine.substring(strIndex + 1).split(RegExp("[ ()]+")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
//Determine if op represents the full string | ||
return [splitRef[0], splitRef.length > 1 || multipleLines]; | ||
} | ||
strIndex = nextOpen; | ||
} | ||
} | ||
return ["", 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"); | ||
|
@@ -145,8 +202,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 = []; | ||
int currLength = currWord.length; | ||
for (String schemeWord in wordToDocs.keys) { | ||
if (((schemeWord.length > currWord.length) && !fullWord) || | ||
schemeWord.length == currWord.length) { | ||
if (schemeWord.substring(0, currLength) == currWord) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The declared name in line 208 isn't really much shorter and doesn't save any repeated work (plus we already use |
||
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> inputText = | ||
element.text.substring(0, cursorPos).split(RegExp("[(]+")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This regex should probably be commented too |
||
List<String> matchingWords = []; | ||
//Find the last word that was being typed | ||
int currLength = 0; | ||
List output = _currOp(element.text, cursorPos); | ||
List output2 = _currOp(element.text, cursorPos, 2); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment explaining the difference between |
||
String findMatch = output[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noun phrases are generally preferred for non-boolean variables. |
||
bool fullWord = output[1]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise, Dart prefers that booleans are named with a form of "to be" most of the time.
|
||
if (findMatch.isEmpty) { | ||
findMatch = output2[0]; | ||
fullWord = output2[1]; | ||
} | ||
if (findMatch.isNotEmpty && inputText.length > 1) { | ||
matchingWords = _wordMatches(findMatch, fullWord); | ||
currLength = findMatch.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 (_enableAutocomplete) { | ||
_autocomplete(); | ||
} | ||
if (key == KeyCode.BACKSPACE) { | ||
await delay(0); | ||
parenListener(missingParens); | ||
|
@@ -155,6 +279,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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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']); | ||
|
@@ -38,6 +37,12 @@ class Repl { | |
status = SpanElement()..classes = ['repl-status']; | ||
container.append(status); | ||
buildNewInput(); | ||
if (window.localStorage.containsKey('autocomplete')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To maintain consistency with the rest of the project, prefix this property with Background: Eventually (probably as part of #48), I'd like to set up some sort of proper namespacing for local storage (or maybe just switch to IndexedDB), but for now, I'd like to keep new interpreter state properties consistent. |
||
String val = window.localStorage['autocomplete']; | ||
if (val == 't') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we can't use actual booleans here due to limitations of localStorage, I'd prefer to use more descriptive properties here like |
||
activeInput.enableAutocomplete(); | ||
} | ||
} | ||
interpreter.logger = (logging, [newline = true]) { | ||
var box = SpanElement(); | ||
activeLoggingArea.append(box); | ||
|
@@ -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'] = 't'; | ||
return undefined; | ||
}, 0); | ||
addBuiltin(env, const SchemeSymbol('disable-autocomplete'), (_a, _b) { | ||
logText('Autocomplete disabled\n'); | ||
activeInput.disableAutocomplete(); | ||
window.localStorage['autocomplete'] = 'f'; | ||
return undefined; | ||
}, 0); | ||
} | ||
|
||
buildNewInput() { | ||
|
@@ -90,8 +108,8 @@ class Repl { | |
..text = prompt | ||
..classes = ['repl-prompt']; | ||
container.append(activePrompt); | ||
activeInput = | ||
CodeInput(container, runCode, parenListener: updateParenStatus); | ||
activeInput = CodeInput(container, runCode, interpreter.globalEnv, | ||
parenListener: updateParenStatus); | ||
container.scrollTop = container.scrollHeight; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this comment is describing exactly. If there's a better phrasing, then use that; otherwise I think it's fine to omit this comment.
Also, style-wise, it's preferred to have a space between
//
or///
and the actual start of the comment (e.g.// Create
)