-
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
Merged
Merged
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 a902410
hide and display the autocomplete box
dnachum 0266595
add the matching strings to the autocomplete box
dnachum 523caf3
fixed bug when adding new lines
dnachum b95a844
added matching prefixes being bolded
dnachum 5b8313f
Merge branch 'master' of https://github.com/Cal-CS-61A-Staff/dart_sch…
dnachum d1766ca
_wordMatches now compares all documented forms
dnachum f0245e0
fixed the width and made the height of the autocomplete box variable
dnachum c0d5ea5
fixed border around words in autobox
dnachum 569f2ba
Make the autobox invisible when displaying docs
dnachum 1f5f0fd
docs stay up after one space to handle cases like cons vs cons-stream
dnachum de7ac76
now looks at the last symbol next to an open parens to decide what do…
dnachum 324e280
fixed autobox disappearing on empty parens
dnachum 238618c
rewrote and cleaned up the _currOp function
dnachum 54a499f
finds both the last op and the second to last op
dnachum 0d426a9
changed the logic for determining if an operation represents a string…
dnachum a361b10
wrapped the autobox with a render element
dnachum 86f01fd
removed the wrapper autBox instead of just the child nodes
dnachum 7e50b67
allow autocomplete to be toggled on and off
dnachum 40aab92
autocomplete toggle remembered in different sessions
dnachum f09f963
will complete word on tab if the docs for that word are displayed
dnachum 2b66225
fixed spelling error with local storage of autocomplete preference
dnachum 8ffb398
scroll the autobox into view if it expands below the current window
dnachum de1031b
made some syntax/naming fixes
dnachum 126f612
cleared up stuff with local storage
dnachum 68d95fd
cleaned up comment formatting and removed unnecesary code
dnachum d2f762d
Used Tuple2 instead of a generic List
dnachum a4fdd3a
changed some variable names
dnachum 3fc75ba
fixed some comments and fixed bug when there exists a space after a o…
dnachum 49e1f90
fixed some variable names and cleared up some comments
dnachum a21f8d3
added dependency and fixed an error with a space before the name of a…
dnachum c1e48d1
fixed boolean variable naming
dnachum File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
|
@@ -14,22 +17,35 @@ const List<SchemeSymbol> noIndentForms = [ | |
SchemeSymbol("define-macro") | ||
]; | ||
|
||
bool _isEnabledAutocomplete = false; | ||
|
||
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); | ||
|
@@ -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) { | ||
|
@@ -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("[ ()]+")); | ||
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. |
||
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) { | ||
|
@@ -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)) { | ||
|
@@ -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); | ||
|
@@ -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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Nit:
_isAutocompleteEnabled
is probably a better name here