Skip to content

Commit

Permalink
Bracket matching (#50)
Browse files Browse the repository at this point in the history
* Adds abstractions for bracket matching

* Disables auto closing brackets during typeahead

* Adds segment factory

* Changes inline renderer listener to use segments to paint

* Separates rendering of brackets from normal buffer

* Renders closing bracket separately from the rest of the suggestion

* Highlights bracket pairs

* Adds logic to insert closing bracket on premature preview termination

* Terminates session should caret have moved prior to suggestion is rendered

* Adds logic to check the number characters deleted

* Clears suggestion segments prior to priming for new suggestion

* Fixes premture preview termination

* Dedupes typed ahead content from acceptance insertion

* Changes color of the auto closing brackets

* Corrects logic for backspacing during typeahead

* Fixes caret jumps from editor breaking preview

* Untrims first line of suggestion

* Adds workbench listener to revert user settings changed on shutdown

* Fixes checkstyle errors

* Accommodates for scenarios where editor and suggestions do not agree on whitespace format

* Improves suggestion text sanitization logic

* Fixes checkstyle error

* Chanages bracket type NADA member to NONE

---------

Co-authored-by: Jonathan Breedlove <[email protected]>
  • Loading branch information
dingfeli and breedloj authored Oct 4, 2024
1 parent f1b0a8d commit cc12271
Show file tree
Hide file tree
Showing 13 changed files with 800 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ public final Object execute(final ExecutionEvent event) throws ExecutionExceptio

private void insertSuggestion(final String suggestion) {
try {
var viewer = QInvocationSession.getInstance().getViewer();
var qSes = QInvocationSession.getInstance();
var viewer = qSes.getViewer();
IDocument doc = viewer.getDocument();
var widget = viewer.getTextWidget();
var insertOffset = widget.getCaretOffset();
doc.replace(insertOffset, 0, suggestion);
widget.setCaretOffset(insertOffset + suggestion.length());
int startIdx = widget.getCaretOffset() - qSes.getInvocationOffset();
String adjustedSuggestion = suggestion.substring(startIdx);
doc.replace(insertOffset, 0, adjustedSuggestion);
widget.setCaretOffset(insertOffset + adjustedSuggestion.length());
QInvocationSession.getInstance().getViewer().getTextWidget().redraw();
QInvocationSession.getInstance().executeCallbackForCodeReference();
QInvocationSession.getInstance().end();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
public enum CaretMovementReason {
UNEXAMINED,
MOVEMENT_KEY,
TEXT_INPUT
TEXT_INPUT,
MOUSE,
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.

package software.aws.toolkits.eclipse.amazonq.util;

import org.eclipse.ui.services.IDisposable;

public interface IQInlineBracket extends IDisposable {
void onTypeOver();

void onDelete();

void pairUp(IQInlineBracket partner);

boolean hasPairedUp();

String getAutoCloseContent(boolean isBracketSetToAutoClose, boolean isBracesSetToAutoClose,
boolean isStringSetToAutoClose);

int getRelevantOffset();

char getSymbol();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.

package software.aws.toolkits.eclipse.amazonq.util;

import org.eclipse.swt.graphics.GC;

public interface IQInlineSuggestionSegment {
void render(GC gc, int currentCaretOffset);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.

package software.aws.toolkits.eclipse.amazonq.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public final class IQInlineSuggestionSegmentFactory {

private IQInlineSuggestionSegmentFactory() {
}

private enum BracketType {
OPEN, CLOSE, NONE;
}

public static List<IQInlineSuggestionSegment> getSegmentsFromSuggestion(final QInvocationSession qSes) {
var suggestion = qSes.getCurrentSuggestion().getInsertText();
var suggestionLines = suggestion.split("\\R");
var res = new ArrayList<IQInlineSuggestionSegment>();
var widget = qSes.getViewer().getTextWidget();
int currentOffset = widget.getCaretOffset();
int distanceTraversed = 0;
Stack<QInlineSuggestionOpenBracketSegment> unresolvedBrackets = new Stack<>();
for (int i = 0; i < suggestionLines.length; i++) {
int startOffset;
int endOffset;
String currentLine = suggestionLines[i];
StringBuilder sb;

startOffset = currentOffset + distanceTraversed; // this line might not exist yet so we need to think of
// something more robust
sb = new StringBuilder(currentLine);

String currentIndent;
if (i == 0) {
int currentLineInDoc = widget.getLineAtOffset(currentOffset);
String content = widget.getLine(currentLineInDoc);
int leadingWhitespacePosition = !content.isEmpty() ? idxOfFirstNonwhiteSpace(content) : 0;
currentIndent = content.substring(0, leadingWhitespacePosition);
} else {
int leadingWhitespacePosition = idxOfFirstNonwhiteSpace(currentLine);
currentIndent = currentLine.substring(0, leadingWhitespacePosition);
}
for (int j = 0; j < currentLine.length(); j++) {
char c = currentLine.charAt(j);
switch (getBracketType(unresolvedBrackets, suggestion, distanceTraversed + j)) {
case OPEN:
var openBracket = new QInlineSuggestionOpenBracketSegment(startOffset + j, currentIndent, c);
unresolvedBrackets.push(openBracket);
break;
case CLOSE:
if (!unresolvedBrackets.isEmpty()) {
var closeBracket = new QInlineSuggestionCloseBracketSegment(startOffset + j, i,
currentLine.substring(0, j), c);
var top = unresolvedBrackets.pop();
if (top.isAMatch(closeBracket)) {
top.pairUp(closeBracket);
sb.setCharAt(j, ' ');
res.add(closeBracket);
res.add(top);
} else {
top.dispose();
closeBracket.dispose();
}
}
break;
case NONE:
default:
continue;
}
}
distanceTraversed += sb.length() + 1; // plus one because we got rid of a \\R when we split it
endOffset = startOffset + sb.length() - 1;
res.add(new QInlineSuggestionNormalSegment(startOffset, endOffset, i, sb.toString()));
}
return res;
}

private static BracketType getBracketType(final Stack<QInlineSuggestionOpenBracketSegment> unresolvedBrackets,
final String input, final int idx) {
if (isCloseBracket(input, idx, unresolvedBrackets)) {
// TODO: enrich logic here to eliminate false positive
return BracketType.CLOSE;
} else if (isOpenBracket(input, idx)) {
// TODO: enrich logic here to eliminate false positive
return BracketType.OPEN;
}
return BracketType.NONE;
}

private static boolean isCloseBracket(final String input, final int idx,
final Stack<QInlineSuggestionOpenBracketSegment> unresolvedBrackets) {
char c = input.charAt(idx);
boolean isBracket = c == ')' || c == ']' || c == '}' || c == '>' || c == '"' || c == '\'';
if (!isBracket) {
return false;
}
if (c == '"' || c == '\'') {
return !unresolvedBrackets.isEmpty() && unresolvedBrackets.peek().getSymbol() == c;
}
// TODO: enrich this check to eliminate false positives
if (idx > 0 && Character.isWhitespace(input.charAt(idx - 1)) && c == '>') {
return false;
}
return true;
}

private static boolean isOpenBracket(final String input, final int idx) {
char c = input.charAt(idx);
boolean isBracket = c == '(' || c == '[' || c == '{' || c == '<' || c == '"' || c == '\'';
if (!isBracket) {
return false;
}
// TODO: enrich this check to eliminate false postives
if (idx > 0 && Character.isWhitespace(input.charAt(idx - 1)) && c == '<') {
return false;
}
return true;
}

private static int idxOfFirstNonwhiteSpace(final String input) {
for (int i = 0; i < input.length(); i++) {
if (input.charAt(i) != ' ' && input.charAt(i) != '\t') {
return i;
}
}
return input.length();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public final class QInlineCaretListener implements CaretListener {
private StyledText widget = null;
private int previousLine = -1;
private int previousLine;

public QInlineCaretListener(final StyledText widget) {
this.widget = widget;
Expand All @@ -27,12 +27,15 @@ public void caretMoved(final CaretEvent event) {
return;
}

if (qInvocationSessionInstance.isPreviewingSuggestions()) {
if (qInvocationSessionInstance.isPreviewingSuggestions()
&& caretMovementReason != CaretMovementReason.UNEXAMINED) {
qInvocationSessionInstance.transitionToDecisionMade(previousLine + 1);
qInvocationSessionInstance.end();
return;
}
}

previousLine = widget.getCaretOffset();
public int getLastKnownLine() {
return previousLine;
}
}
Loading

0 comments on commit cc12271

Please sign in to comment.