From cc12271ee83a14a5d8212099e6500a37e8818ca0 Mon Sep 17 00:00:00 2001 From: Felix Ding Date: Thu, 3 Oct 2024 17:32:55 -0700 Subject: [PATCH] Bracket matching (#50) * 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 --- .../handlers/QAcceptSuggestionsHandler.java | 9 +- .../amazonq/util/CaretMovementReason.java | 3 +- .../eclipse/amazonq/util/IQInlineBracket.java | 22 ++ .../util/IQInlineSuggestionSegment.java | 9 + .../IQInlineSuggestionSegmentFactory.java | 131 ++++++++ .../amazonq/util/QInlineCaretListener.java | 9 +- .../amazonq/util/QInlineInputListener.java | 288 +++++++++++------- .../amazonq/util/QInlineRendererListener.java | 76 +---- .../QInlineSuggestionCloseBracketSegment.java | 117 +++++++ .../util/QInlineSuggestionNormalSegment.java | 58 ++++ .../QInlineSuggestionOpenBracketSegment.java | 129 ++++++++ .../amazonq/util/QInvocationSession.java | 76 ++++- .../amazonq/util/SuggestionTextUtil.java | 55 ++++ 13 files changed, 800 insertions(+), 182 deletions(-) create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineBracket.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegment.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegmentFactory.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionCloseBracketSegment.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionNormalSegment.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionOpenBracketSegment.java create mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/util/SuggestionTextUtil.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptSuggestionsHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptSuggestionsHandler.java index 817a0063..d779b3f0 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptSuggestionsHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptSuggestionsHandler.java @@ -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(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CaretMovementReason.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CaretMovementReason.java index 813e59d1..7315c01d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CaretMovementReason.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CaretMovementReason.java @@ -5,6 +5,7 @@ public enum CaretMovementReason { UNEXAMINED, MOVEMENT_KEY, - TEXT_INPUT + TEXT_INPUT, + MOUSE, } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineBracket.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineBracket.java new file mode 100644 index 00000000..2b43222f --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineBracket.java @@ -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(); +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegment.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegment.java new file mode 100644 index 00000000..f833ac76 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegment.java @@ -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); +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegmentFactory.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegmentFactory.java new file mode 100644 index 00000000..c98c3866 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/IQInlineSuggestionSegmentFactory.java @@ -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 getSegmentsFromSuggestion(final QInvocationSession qSes) { + var suggestion = qSes.getCurrentSuggestion().getInsertText(); + var suggestionLines = suggestion.split("\\R"); + var res = new ArrayList(); + var widget = qSes.getViewer().getTextWidget(); + int currentOffset = widget.getCaretOffset(); + int distanceTraversed = 0; + Stack 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 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 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(); + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineCaretListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineCaretListener.java index dc9dda9a..e764849c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineCaretListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineCaretListener.java @@ -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; @@ -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; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java index 280fbb2f..ba6c6158 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java @@ -2,33 +2,148 @@ package software.aws.toolkits.eclipse.amazonq.util; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; + +public final class QInlineInputListener implements VerifyListener, VerifyKeyListener, MouseListener { -public final class QInlineInputListener implements VerifyListener, VerifyKeyListener { private StyledText widget = null; private int distanceTraversed = 0; - private boolean isAutoClosingEnabled = true; + private int numSuggestionLines = 0; private LastKeyStrokeType lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; + private boolean isBracketsSetToAutoClose = false; + private boolean isBracesSetToAutoClose = false; + private boolean isStringSetToAutoClose = false; + private List suggestionSegments = new ArrayList<>(); + private IQInlineBracket[] brackets; private enum LastKeyStrokeType { - NORMAL_INPUT, BACKSPACE, NORMAL_BRACKET, CURLY_BRACES, OPEN_CURLY, OPEN_CURLY_FOLLOWED_BY_NEW_LINE, + NORMAL_INPUT, BACKSPACE, } + /** + * During instantiation we would need to perform the following to prime the + * listeners for typeahead: + *
    + *
  • Set these auto closing settings to false.
  • + *
  • Analyze the buffer in current suggestions for bracket pairs.
  • + *
+ * + * @param widget + */ public QInlineInputListener(final StyledText widget) { IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.ui"); // This needs to be defaulted to true. This key is only present in the // preference store if it is set to false. // Therefore if you can't find it, it has been set to true. - this.isAutoClosingEnabled = preferences.getBoolean("closeBrackets", true); + isBracesSetToAutoClose = preferences.getBoolean("closeBraces", true); + isBracketsSetToAutoClose = preferences.getBoolean("closeBrackets", true); + isStringSetToAutoClose = preferences.getBoolean("closeStrings", true); + preferences.putBoolean("closeBraces", false); + preferences.putBoolean("closeBrackets", false); + preferences.putBoolean("closeStrings", false); this.widget = widget; } + /** + * A routine to prime the class for typeahead related information. These are: + *
    + *
  • Where each bracket pairs are.
  • + *
+ * + * This is to be called on instantiation as well as when new suggestion has been + * toggled to. + */ + public void onNewSuggestion() { + lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; + var qInvocationSessionInstance = QInvocationSession.getInstance(); + if (qInvocationSessionInstance == null) { + return; + } + if (!suggestionSegments.isEmpty()) { + suggestionSegments.clear(); + } + numSuggestionLines = qInvocationSessionInstance.getCurrentSuggestion().getInsertText().split("\\R").length; + List segments = IQInlineSuggestionSegmentFactory + .getSegmentsFromSuggestion(qInvocationSessionInstance); + brackets = new IQInlineBracket[qInvocationSessionInstance.getCurrentSuggestion().getInsertText().length()]; + int invocationOffset = qInvocationSessionInstance.getInvocationOffset(); + for (var segment : segments) { + if (segment instanceof IQInlineBracket) { + int offset = ((IQInlineBracket) segment).getRelevantOffset(); + int idxInSuggestion = offset - invocationOffset; + if (((IQInlineBracket) segment).getSymbol() == '{') { + int firstNewLineAfter = qInvocationSessionInstance.getCurrentSuggestion().getInsertText() + .indexOf('\n', idxInSuggestion); + brackets[firstNewLineAfter] = (IQInlineBracket) segment; + } else { + brackets[idxInSuggestion] = (IQInlineBracket) segment; + } + // We only add close brackets to be rendered separately + if (segment instanceof QInlineSuggestionCloseBracketSegment) { + suggestionSegments.add(segment); + } + } else { + suggestionSegments.add(segment); + } + } + } + + public List getSegments() { + return suggestionSegments; + } + + /** + * Here we need to perform the following before the listener gets removed: + *
    + *
  • If the auto closing of brackets was enabled originally, we should add these closed brackets back into the buffer.
  • + *
  • Revert the settings back to their original states.
  • + *
+ */ + public void beforeRemoval() { + var qSes = QInvocationSession.getInstance(); + if (qSes == null || !qSes.isActive() || brackets == null) { + return; + } + String toAppend = ""; + for (int i = brackets.length - 1; i >= 0; i--) { + var bracket = brackets[i]; + if (bracket == null) { + continue; + } + String autoCloseContent = bracket.getAutoCloseContent(isBracketsSetToAutoClose, isBracesSetToAutoClose, + isStringSetToAutoClose); + if (autoCloseContent != null) { + toAppend += autoCloseContent; + } + bracket.dispose(); + } + + IDocument doc = qSes.getViewer().getDocument(); + try { + doc.replace(qSes.getInvocationOffset() + distanceTraversed, 0, toAppend); + } catch (BadLocationException e) { + PluginLogger.error(e.toString()); + } + + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.ui"); + preferences.putBoolean("closeBraces", isBracesSetToAutoClose); + preferences.putBoolean("closeBrackets", isBracketsSetToAutoClose); + preferences.putBoolean("closeStrings", isStringSetToAutoClose); + } + @Override public void verifyKey(final VerifyEvent event) { var qInvocationSessionInstance = QInvocationSession.getInstance(); @@ -53,21 +168,12 @@ public void verifyKey(final VerifyEvent event) { // preview's lifetime: // - CR (new line) // - BS (backspace) - String currentSuggestion = qInvocationSessionInstance.getCurrentSuggestion().getInsertText().trim(); switch (event.keyCode) { case SWT.CR: - if (lastKeyStrokeType == LastKeyStrokeType.OPEN_CURLY && isAutoClosingEnabled) { - lastKeyStrokeType = LastKeyStrokeType.OPEN_CURLY_FOLLOWED_BY_NEW_LINE; - // we need to unset the vertical indent prior to new line otherwise the line inserted by - // eclipse with the closing curly braces would inherit the extra vertical indent. - int line = widget.getLineAtOffset(widget.getCaretOffset()); - qInvocationSessionInstance.unsetVerticalIndent(line + 1); - } else { - lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; - } + lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; return; case SWT.BS: - if (--distanceTraversed < 0) { + if (distanceTraversed == 0) { qInvocationSessionInstance.transitionToDecisionMade(); qInvocationSessionInstance.end(); return; @@ -79,107 +185,31 @@ public void verifyKey(final VerifyEvent event) { qInvocationSessionInstance.end(); return; default: - } - - // If auto closing of brackets are not enabled we can just treat them as normal - // inputs - // Another scenario - if (!isAutoClosingEnabled) { - return; - } - - // If auto cloising of brackets are enabled, SWT will treat the open bracket - // differently. - // Input of the brackets will not trigger a call to verifyText. - // Thus we have to do the typeahead verification here. - // Note that '{' is excluded because - switch (event.character) { - case '<': - if (currentSuggestion.charAt(distanceTraversed++) != '<') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - return; - case '>': - if (currentSuggestion.charAt(distanceTraversed++) != '>') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - return; - case '(': - if (currentSuggestion.charAt(distanceTraversed++) != '(') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - return; - case ')': - if (currentSuggestion.charAt(distanceTraversed++) != ')') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; + lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; return; - case '[': - if (currentSuggestion.charAt(distanceTraversed++) != '[') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - return; - case ']': - if (currentSuggestion.charAt(distanceTraversed++) != ']') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - return; - case '{': - if (currentSuggestion.charAt(distanceTraversed++) != '{') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.OPEN_CURLY; - return; - case '}': - if (currentSuggestion.charAt(distanceTraversed++) != '}') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.CURLY_BRACES; - return; - case '"': - if (currentSuggestion.charAt(distanceTraversed++) != '"') { - qInvocationSessionInstance.transitionToDecisionMade(); - qInvocationSessionInstance.end(); - return; - } - lastKeyStrokeType = LastKeyStrokeType.NORMAL_BRACKET; - default: } - - lastKeyStrokeType = LastKeyStrokeType.NORMAL_INPUT; } @Override public void verifyText(final VerifyEvent event) { - String input = event.text; switch (lastKeyStrokeType) { case NORMAL_INPUT: break; - case OPEN_CURLY_FOLLOWED_BY_NEW_LINE: - input = '\n' + event.text.split("\\R")[1]; - break; + case BACKSPACE: + var qInvocationSessionInstance = QInvocationSession.getInstance(); + int numCharDeleted = event.end - event.start; + if (numCharDeleted > distanceTraversed) { + qInvocationSessionInstance.transitionToDecisionMade(); + qInvocationSessionInstance.end(); + } + for (int i = 1; i <= numCharDeleted; i++) { + var bracket = brackets[distanceTraversed - i]; + if (bracket != null) { + bracket.onDelete(); + } + } + distanceTraversed -= numCharDeleted; + return; default: return; } @@ -189,17 +219,29 @@ public void verifyText(final VerifyEvent event) { return; } - String currentSuggestion = qInvocationSessionInstance.getCurrentSuggestion().getInsertText().trim(); + String currentSuggestion = qInvocationSessionInstance.getCurrentSuggestion().getInsertText(); + String input = event.text; int currentOffset = widget.getCaretOffset(); qInvocationSessionInstance .setHasBeenTypedahead(currentOffset - qInvocationSessionInstance.getInvocationOffset() > 0); boolean isOutOfBounds = distanceTraversed >= currentSuggestion.length() || distanceTraversed < 0; if (isOutOfBounds || !isInputAMatch(currentSuggestion, distanceTraversed, input)) { +// System.out.println("input is: " +// + input.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace(' ', 's')); +// System.out.println("suggestion is: " +// + currentSuggestion.substring(distanceTraversed, distanceTraversed + input.length()) +// .replace("\n", "\\n").replace("\r", "\\r".replace("\t", "\\t").replace(' ', 's'))); qInvocationSessionInstance.transitionToDecisionMade(); qInvocationSessionInstance.end(); return; } + for (int i = distanceTraversed; i < distanceTraversed + input.length(); i++) { + var bracket = brackets[i]; + if (bracket != null) { + bracket.onTypeOver(); + } + } distanceTraversed += input.length(); } @@ -207,10 +249,38 @@ private boolean isInputAMatch(final String currentSuggestion, final int startIdx boolean res; if (input.length() > 1) { res = currentSuggestion.substring(startIdx, startIdx + input.length()).equals(input); - System.out.println("This is a match: " + res); } else { res = String.valueOf(currentSuggestion.charAt(startIdx)).equals(input); } return res; } + + public int getNumSuggestionLines() { + return numSuggestionLines; + } + + @Override + public void mouseDoubleClick(final MouseEvent e) { + return; + } + + @Override + public void mouseDown(final MouseEvent e) { + // For the most part setting status here is pointless (for now) + // This is because the only other component that is relying on + // CaretMovementReason + // (the CaretListener) is called _before_ the mouse listener + // For consistency sake, we'll stick with updating it now. + var qInvocationSessionInstance = QInvocationSession.getInstance(); + qInvocationSessionInstance.setCaretMovementReason(CaretMovementReason.MOUSE); + int lastKnownLine = qInvocationSessionInstance.getLastKnownLine(); + qInvocationSessionInstance.transitionToDecisionMade(lastKnownLine + 1); + qInvocationSessionInstance.end(); + return; + } + + @Override + public void mouseUp(final MouseEvent e) { + return; + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineRendererListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineRendererListener.java index bc84035e..4a68a5fa 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineRendererListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineRendererListener.java @@ -7,14 +7,9 @@ import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Point; -import static software.aws.toolkits.eclipse.amazonq.util.QConstants.Q_INLINE_HINT_TEXT_COLOR; import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.shouldIndentVertically; -import java.util.Arrays; - public class QInlineRendererListener implements PaintListener { - private int currentLine = -1; - private int offsetAtCurrentLine = -1; @Override public final void paintControl(final PaintEvent e) { @@ -24,64 +19,23 @@ public final void paintControl(final PaintEvent e) { } var gc = e.gc; - gc.setForeground(Q_INLINE_HINT_TEXT_COLOR); - gc.setFont(QInvocationSession.getInstance().getInlineTextFont()); - var widget = QInvocationSession.getInstance().getViewer().getTextWidget(); - - var location = widget.getLocationAtOffset(widget.getCaretOffset()); - var suggestion = QInvocationSession.getInstance().getCurrentSuggestion().getInsertText(); - int invocationOffset = QInvocationSession.getInstance().getInvocationOffset(); - var suggestionParts = suggestion.split("\\R"); - - int currentOffset = widget.getCaretOffset(); - int lineOffset = widget.getLineAtOffset(currentOffset); - int originalLine = widget.getLineAtOffset(invocationOffset); - int currentLineInSuggestion = lineOffset - originalLine; - if (currentLine < lineOffset) { - // this accounts for a traversal "downwards" as user types - currentLine = lineOffset; - offsetAtCurrentLine = currentOffset; - qInvocationSessionInstance.setHeadOffsetAtLine(lineOffset, currentOffset); - } else if (currentLine > lineOffset) { - // this accounts for a traversal "upwards" as user backspaces - currentLine = lineOffset; - offsetAtCurrentLine = qInvocationSessionInstance.getHeadOffsetAtLine(lineOffset); - } - - int renderHeadIndex = currentOffset - offsetAtCurrentLine; - String[] remainderArray = Arrays.copyOfRange(suggestionParts, currentLineInSuggestion + 1, - suggestionParts.length); - String remainder = String.join("\n", remainderArray); - - // Draw first line inline - String firstLine = renderHeadIndex >= 0 ? suggestionParts[currentLineInSuggestion].trim() - : suggestionParts[currentLineInSuggestion]; - int xLoc = renderHeadIndex >= 0 ? location.x : widget.getLeftMargin(); - if (renderHeadIndex < firstLine.length()) { - gc.drawText(renderHeadIndex >= 0 ? firstLine.substring(renderHeadIndex) : firstLine, xLoc, location.y, - true); + var widget = qInvocationSessionInstance.getViewer().getTextWidget(); + var invocationLine = widget.getLineAtOffset(qInvocationSessionInstance.getInvocationOffset()); + var segments = qInvocationSessionInstance.getSegments(); + var caretLine = widget.getLineAtOffset(widget.getCaretOffset()); + int numSuggestionLines = qInvocationSessionInstance.getNumSuggestionLines(); + + if (shouldIndentVertically(widget, caretLine) + && qInvocationSessionInstance.isPreviewingSuggestions()) { + Point textExtent = gc.stringExtent(" "); + int height = textExtent.y * (numSuggestionLines - (caretLine - invocationLine) - 1); + qInvocationSessionInstance.setVerticalIndent(caretLine + 1, height); + } else if (caretLine + 1 == (invocationLine + numSuggestionLines)) { + qInvocationSessionInstance.unsetVerticalIndent(caretLine + 1); } - // Draw other lines inline - if (!remainder.isEmpty()) { - // For last line case doesn't need to indent next line vertically - var caretLine = widget.getLineAtOffset(widget.getCaretOffset()); - if (shouldIndentVertically(widget, caretLine) && qInvocationSessionInstance.isPreviewingSuggestions()) { - // when showing the suggestion need to add next line indent - Point textExtent = gc.stringExtent(" "); - int height = textExtent.y * remainder.split("\\R").length; - qInvocationSessionInstance.setVerticalIndent(caretLine + 1, height); - } - - int lineHt = widget.getLineHeight(); - int fontHt = gc.getFontMetrics().getHeight(); - int x = widget.getLeftMargin(); - int y = location.y + lineHt * 2 - fontHt; - gc.drawText(remainder, x, y, true); - } else { - int line = widget.getLineAtOffset(widget.getCaretOffset()); - qInvocationSessionInstance.unsetVerticalIndent(line + 1); + for (int i = 0; i < segments.size(); i++) { + segments.get(i).render(gc, widget.getCaretOffset()); } } - } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionCloseBracketSegment.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionCloseBracketSegment.java new file mode 100644 index 00000000..aba02af1 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionCloseBracketSegment.java @@ -0,0 +1,117 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +package software.aws.toolkits.eclipse.amazonq.util; + +import static software.aws.toolkits.eclipse.amazonq.util.QConstants.Q_INLINE_HINT_TEXT_COLOR; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.GC; + +public final class QInlineSuggestionCloseBracketSegment implements IQInlineSuggestionSegment, IQInlineBracket { + private QInlineSuggestionOpenBracketSegment openBracket; + private char symbol; + private int caretOffset; + private int lineInSuggestion; + private String text; + private Font adjustedTypedFont; + + public QInlineSuggestionCloseBracketSegment(final int caretOffset, final int lineInSuggestion, final String text, + final char symbol) { + this.caretOffset = caretOffset; + this.symbol = symbol; + this.lineInSuggestion = lineInSuggestion; + this.text = text; + + var qInvocationSessionInstance = QInvocationSession.getInstance(); + var widget = qInvocationSessionInstance.getViewer().getTextWidget(); + Font typedFont = widget.getFont(); + var fontData = typedFont.getFontData(); + for (var fd : fontData) { + fd.setStyle(fd.getStyle() | SWT.BOLD); + } + adjustedTypedFont = new Font(widget.getDisplay(), fontData); + } + + @Override + public void pairUp(final IQInlineBracket openBracket) { + this.openBracket = (QInlineSuggestionOpenBracketSegment) openBracket; + if (!openBracket.hasPairedUp()) { + this.openBracket.pairUp(this); + } + } + + @Override + public boolean hasPairedUp() { + return openBracket != null; + } + + @Override + public void render(final GC gc, final int currentCaretOffset) { + if (currentCaretOffset > caretOffset) { + return; + } + var qInvocationSessionInstance = QInvocationSession.getInstance(); + if (qInvocationSessionInstance == null) { + return; + } + var widget = qInvocationSessionInstance.getViewer().getTextWidget(); + + int x; + int y; + int invocationOffset = qInvocationSessionInstance.getInvocationOffset(); + int invocationLine = widget.getLineAtOffset(invocationOffset); + int lineHt = widget.getLineHeight(); + int fontHt = gc.getFontMetrics().getHeight(); + // educated guess: + int endPadding = gc.getAdvanceWidth(symbol) / 4; + y = (invocationLine + lineInSuggestion + 1) * lineHt - fontHt; + x = gc.textExtent(text).x + endPadding; + if (lineInSuggestion == 0) { + x += widget.getLocationAtOffset(invocationOffset).x; + } + + if (currentCaretOffset > openBracket.getRelevantOffset()) { + Color typedColor = widget.getForeground(); + gc.setForeground(typedColor); + gc.setFont(adjustedTypedFont); + } else { + gc.setForeground(Q_INLINE_HINT_TEXT_COLOR); + gc.setFont(qInvocationSessionInstance.getInlineTextFont()); + } + gc.drawText(String.valueOf(symbol), x, y, true); + } + + @Override + public void onTypeOver() { + openBracket.setResolve(true); + } + + @Override + public void onDelete() { + openBracket.setResolve(true); + } + + @Override + public String getAutoCloseContent(final boolean isBracketSetToAutoClose, final boolean isBracesSetToAutoClose, + final boolean isStringSetToAutoClose) { + // This is a noop for close brackets + return null; + } + + @Override + public int getRelevantOffset() { + return caretOffset; + } + + @Override + public char getSymbol() { + return symbol; + } + + @Override + public void dispose() { + adjustedTypedFont.dispose(); + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionNormalSegment.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionNormalSegment.java new file mode 100644 index 00000000..ef50f7c9 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionNormalSegment.java @@ -0,0 +1,58 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +package software.aws.toolkits.eclipse.amazonq.util; + +import static software.aws.toolkits.eclipse.amazonq.util.QConstants.Q_INLINE_HINT_TEXT_COLOR; + +import org.eclipse.swt.graphics.GC; + +public final class QInlineSuggestionNormalSegment implements IQInlineSuggestionSegment { + private int startCaretOffset; + private int endCaretOffset; + private int lineInSuggestion; + private String text; + + public QInlineSuggestionNormalSegment(final int startCaretPosition, final int endCaretPosition, + final int lineInSuggestion, final String text) { + this.text = text; + this.startCaretOffset = startCaretPosition; + this.endCaretOffset = endCaretPosition; + this.lineInSuggestion = lineInSuggestion; + } + + @Override + public void render(final GC gc, final int currentCaretOffset) { + if (currentCaretOffset > endCaretOffset) { + return; + } + var qInvocationSessionInstance = QInvocationSession.getInstance(); + if (qInvocationSessionInstance == null) { + return; + } + var widget = qInvocationSessionInstance.getViewer().getTextWidget(); + + int x; + int y; + String textToRender; + int invocationLine = widget.getLineAtOffset(qInvocationSessionInstance.getInvocationOffset()); + int lineHt = widget.getLineHeight(); + int fontHt = gc.getFontMetrics().getHeight(); + y = (invocationLine + lineInSuggestion + 1) * lineHt - fontHt; + + int idxInLine = currentCaretOffset - startCaretOffset; + if (lineInSuggestion == 0) { + x = widget.getLocationAtOffset(widget.getCaretOffset()).x; + textToRender = text.substring(idxInLine); + } else if (currentCaretOffset <= startCaretOffset) { + textToRender = text; + x = widget.getLeftMargin(); + } else { + x = gc.textExtent(text.substring(0, idxInLine)).x + gc.textExtent(" ").x / 4; + textToRender = text.substring(idxInLine); + } + + gc.setForeground(Q_INLINE_HINT_TEXT_COLOR); + gc.setFont(qInvocationSessionInstance.getInlineTextFont()); + gc.drawText(textToRender, x, y, true); + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionOpenBracketSegment.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionOpenBracketSegment.java new file mode 100644 index 00000000..b94fd5f5 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineSuggestionOpenBracketSegment.java @@ -0,0 +1,129 @@ +// 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 final class QInlineSuggestionOpenBracketSegment implements IQInlineSuggestionSegment, IQInlineBracket { + private QInlineSuggestionCloseBracketSegment closeBracket; + private char symbol; + private String indent; + private int caretOffset; + private boolean isResolved = true; + + public QInlineSuggestionOpenBracketSegment(final int caretOffset, final String indent, final char symbol) { + this.caretOffset = caretOffset; + this.symbol = symbol; + this.indent = indent; + } + + @Override + public void pairUp(final IQInlineBracket closeBracket) { + this.closeBracket = (QInlineSuggestionCloseBracketSegment) closeBracket; + if (!closeBracket.hasPairedUp()) { + closeBracket.pairUp((IQInlineBracket) this); + } + } + + public boolean isAMatch(final QInlineSuggestionCloseBracketSegment closeBracket) { + switch (symbol) { + case '<': + return closeBracket.getSymbol() == '>'; + case '{': + return closeBracket.getSymbol() == '}'; + case '(': + return closeBracket.getSymbol() == ')'; + case '"': + return closeBracket.getSymbol() == '"'; + case '\'': + return closeBracket.getSymbol() == '\''; + case '[': + return closeBracket.getSymbol() == ']'; + default: + return false; + } + } + + public void setResolve(final boolean isResolved) { + this.isResolved = isResolved; + } + + public boolean hasPairedUp() { + return closeBracket != null; + } + + @Override + public void render(final GC gc, final int currentCaretOffset) { + // We never separates open brackets from the lines from which they came. + // This is because there is never a need to highlight open brackets. + return; + } + + @Override + public void onTypeOver() { + isResolved = false; + } + + @Override + public void onDelete() { + isResolved = true; + } + + @Override + public String getAutoCloseContent(final boolean isBracketSetToAutoClose, final boolean isBracesSetToAutoClose, + final boolean isStringSetToAutoClose) { + if (isResolved) { + return null; + } + + switch (symbol) { + case '<': + if (!isBracketSetToAutoClose) { + return null; + } + return ">"; + case '{': + if (!isBracesSetToAutoClose) { + return null; + } + return "\n" + indent + "}"; + case '(': + if (!isBracketSetToAutoClose) { + return null; + } + return ")"; + case '"': + if (!isStringSetToAutoClose) { + return null; + } + return "\""; + case '\'': + if (!isStringSetToAutoClose) { + return null; + } + return "'"; + case '[': + if (!isBracketSetToAutoClose) { + return null; + } + return "]"; + default: + return null; + } + } + + @Override + public int getRelevantOffset() { + return caretOffset; + } + + @Override + public char getSymbol() { + return symbol; + } + + @Override + public void dispose() { + return; + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java index 42955376..87d0de4b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.util; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.ITextViewer; import org.eclipse.swt.SWT; @@ -11,6 +13,9 @@ import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchListener; +import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.ITextEditor; import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionItem; import software.aws.toolkits.eclipse.amazonq.providers.LspProvider; @@ -23,6 +28,7 @@ import static software.aws.toolkits.eclipse.amazonq.util.QConstants.Q_INLINE_HINT_TEXT_STYLE; import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextViewer; +import static software.aws.toolkits.eclipse.amazonq.util.SuggestionTextUtil.replaceSpacesWithTabs; public final class QInvocationSession extends QResource { @@ -38,6 +44,7 @@ public final class QInvocationSession extends QResource { private ITextViewer viewer = null; private Font inlineTextFont = null; private int invocationOffset = -1; + private int tabSize; private long invocationTimeInMs = -1L; private QInlineRendererListener paintListener = null; private CaretListener caretListener = null; @@ -45,6 +52,7 @@ public final class QInvocationSession extends QResource { private Stack closingBrackets = new Stack<>(); private int[] headOffsetAtLine = new int[500]; private boolean hasBeenTypedahead = false; + private boolean isTabOnly = false; private CodeReferenceAcceptanceCallback codeReferenceAcceptanceCallback = null; private Consumer unsetVerticalIndent; @@ -57,6 +65,31 @@ private QInvocationSession() { public static synchronized QInvocationSession getInstance() { if (instance == null) { instance = new QInvocationSession(); + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.ui"); + boolean isBracesSetToAutoClose = preferences.getBoolean("closeBraces", true); + boolean isBracketsSetToAutoClose = preferences.getBoolean("closeBrackets", true); + boolean isStringSetToAutoClose = preferences.getBoolean("closeStrings", true); + + // We'll also need tab sizes since suggestions do not take that into account + // and is only given in spaces + IEclipsePreferences tabPref = InstanceScope.INSTANCE.getNode("org.eclipse.jdt.core"); + instance.tabSize = tabPref.getInt("org.eclipse.jdt.core.formatter.tabulation.size", 4); + instance.isTabOnly = tabPref.getBoolean("use_tabs_only_for_leading_indentations", true); + + PlatformUI.getWorkbench().addWorkbenchListener(new IWorkbenchListener() { + @Override + public boolean preShutdown(final IWorkbench workbench, final boolean forced) { + preferences.putBoolean("closeBraces", isBracesSetToAutoClose); + preferences.putBoolean("closeBrackets", isBracketsSetToAutoClose); + preferences.putBoolean("closeStrings", isStringSetToAutoClose); + return true; + } + + @Override + public void postShutdown(final IWorkbench workbench) { + return; + } + }); } return instance; } @@ -96,6 +129,7 @@ public synchronized boolean start(final ITextEditor editor) { inputListener = new QInlineInputListener(widget); widget.addVerifyListener(inputListener); widget.addVerifyKeyListener(inputListener); + widget.addMouseListener(inputListener); caretListener = new QInlineCaretListener(widget); widget.addCaretListener(caretListener); @@ -124,10 +158,19 @@ public void invoke() { } List newSuggestions = LspProvider.getAmazonQServer().get() - .inlineCompletionWithReferences(params).thenApply(result -> result.getItems()).get(); + .inlineCompletionWithReferences(params) + .thenApply(result -> result.getItems().parallelStream().map(item -> { + if (isTabOnly) { + String sanitizedText = replaceSpacesWithTabs(item.getInsertText(), tabSize); + System.out.println("Sanitized text: " + sanitizedText.replace("\n", "\\n").replace("\t", "\\t")); + item.setInsertText(sanitizedText); + } + return item; + }).collect(Collectors.toList())).get(); Display.getDefault().asyncExec(() -> { - if (newSuggestions == null || newSuggestions.isEmpty()) { + if (newSuggestions == null || newSuggestions.isEmpty() || session + .getInvocationOffset() != session.getViewer().getTextWidget().getCaretOffset()) { end(); return; } @@ -136,10 +179,12 @@ public void invoke() { newSuggestions.stream().map(QSuggestionContext::new).collect(Collectors.toList())); suggestionsContext.setCurrentIndex(0); + session.primeListeners(); // TODO: remove print // Update the UI with the results - System.out.println("Suggestions: " + newSuggestions); + System.out.println("Suggestions: " + newSuggestions.stream() + .map(suggestion -> suggestion.getInsertText()).collect(Collectors.toList())); System.out.println("Total suggestion number: " + newSuggestions.size()); transitionToPreviewingState(); @@ -178,8 +223,8 @@ public synchronized void end() { System.out.println(element); } if (isActive()) { - state = QInvocationSessionState.INACTIVE; dispose(); + state = QInvocationSessionState.INACTIVE; // End session logic here System.out.println("Session ended."); } else { @@ -296,6 +341,7 @@ public InlineCompletionItem getCurrentSuggestion() { public void decrementCurrentSuggestionIndex() { if (suggestionsContext != null) { suggestionsContext.decrementIndex(); + primeListeners(); getViewer().getTextWidget().redraw(); } } @@ -303,6 +349,7 @@ public void decrementCurrentSuggestionIndex() { public void incrementCurentSuggestionIndex() { if (suggestionsContext != null) { suggestionsContext.incrementIndex(); + primeListeners(); getViewer().getTextWidget().redraw(); } } @@ -315,7 +362,8 @@ public boolean hasBeenTypedahead() { return hasBeenTypedahead; } - public void registerCallbackForCodeReference(final CodeReferenceAcceptanceCallback codeReferenceAcceptanceCallback) { + public void registerCallbackForCodeReference( + final CodeReferenceAcceptanceCallback codeReferenceAcceptanceCallback) { this.codeReferenceAcceptanceCallback = codeReferenceAcceptanceCallback; } @@ -343,6 +391,22 @@ public void unsetVerticalIndent(final int caretLine) { } } + public List getSegments() { + return inputListener.getSegments(); + } + + public int getNumSuggestionLines() { + return inputListener.getNumSuggestionLines(); + } + + public void primeListeners() { + inputListener.onNewSuggestion(); + } + + public int getLastKnownLine() { + return ((QInlineCaretListener) caretListener).getLastKnownLine(); + } + // Additional methods for the session can be added here @Override public void dispose() { @@ -354,11 +418,13 @@ public void dispose() { closingBrackets = null; caretMovementReason = CaretMovementReason.UNEXAMINED; hasBeenTypedahead = false; + inputListener.beforeRemoval(); QInvocationSession.getInstance().getViewer().getTextWidget().redraw(); widget.removePaintListener(paintListener); widget.removeCaretListener(caretListener); widget.removeVerifyListener(inputListener); widget.removeVerifyKeyListener(inputListener); + widget.removeMouseListener(inputListener); paintListener = null; caretListener = null; inputListener = null; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SuggestionTextUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SuggestionTextUtil.java new file mode 100644 index 00000000..fdbb03d8 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SuggestionTextUtil.java @@ -0,0 +1,55 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +package software.aws.toolkits.eclipse.amazonq.util; + +public final class SuggestionTextUtil { + + private SuggestionTextUtil() { + } + + public static String replaceSpacesWithTabs(final String input, final int tabSize) { + StringBuilder result = new StringBuilder(); + String[] lines = input.split("\\r?\\n"); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + int numSpaces = 0; + StringBuilder newLine = new StringBuilder(); + + for (int j = 0; j < line.length(); j++) { + char c = line.charAt(j); + if (c == ' ') { + numSpaces++; + } else { + newLine.append(getTabsForSpaces(numSpaces, tabSize)); + newLine.append(c); + numSpaces = 0; + } + } + + if (i < lines.length - 1) { + newLine.append("\n"); + } + + result.append(newLine); + } + + return result.toString(); + } + + private static String getTabsForSpaces(final int numSpaces, final int tabSize) { + int numTabs = numSpaces / tabSize; + StringBuilder tabs = new StringBuilder(); + + for (int i = 0; i < numTabs; i++) { + tabs.append("\t"); + } + + int remainingSpaces = numSpaces % tabSize; + for (int i = 0; i < remainingSpaces; i++) { + tabs.append(" "); + } + + return tabs.toString(); + } +}