From fe4e02f7f6abdb1ec056df4fcb956ab499262c8e Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 6 Feb 2024 02:18:53 +0200 Subject: [PATCH] Revert "Revert "feat: code completion improvements"" This reverts commit 7f586da0c185253b8e399ec118ca441c8cfebf3d. --- .../carlrobert/codegpt/EncodingManager.java | 16 +++ .../actions/DisableCompletionsAction.java | 4 +- .../actions/EnableCompletionsAction.java | 4 +- .../CodeCompletionEventListener.java | 5 +- .../CodeCompletionService.java | 85 ++--------- .../codecompletions/InfillRequestDetails.java | 30 ++++ .../CodeCompletionServiceTest.java | 132 ++++++------------ .../codecompletions/code-completion-file.txt | 10 +- 8 files changed, 109 insertions(+), 177 deletions(-) diff --git a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java index d245f66b..d4790dca 100644 --- a/src/main/java/ee/carlrobert/codegpt/EncodingManager.java +++ b/src/main/java/ee/carlrobert/codegpt/EncodingManager.java @@ -9,6 +9,7 @@ import com.knuddels.jtokkit.api.EncodingRegistry; import com.knuddels.jtokkit.api.EncodingType; import ee.carlrobert.codegpt.conversations.Conversation; import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionMessage; +import java.util.List; @Service public final class EncodingManager { @@ -52,4 +53,19 @@ public final class EncodingManager { return 0; } } + + /** + * Truncates the given text to the given number of tokens. + * + * @param text The text to truncate. + * @param maxTokens The maximum number of tokens to keep. + * @param fromStart Whether to truncate from the start or the end of the text. + * @return The truncated text. + */ + public String truncateText(String text, int maxTokens, boolean fromStart) { + List tokens = encoding.encode(text); + int tokensToRetrieve = Math.min(maxTokens, tokens.size()); + int startIndex = fromStart ? 0 : tokens.size() - tokensToRetrieve; + return encoding.decode(tokens.subList(startIndex, startIndex + tokensToRetrieve)); + } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java index 13e444d6..4aa7382b 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/DisableCompletionsAction.java @@ -1,8 +1,8 @@ package ee.carlrobert.codegpt.actions; -import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.DumbAwareAction; import ee.carlrobert.codegpt.codecompletions.CodeGPTEditorManager; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import org.jetbrains.annotations.NotNull; @@ -10,7 +10,7 @@ import org.jetbrains.annotations.NotNull; /** * Disables code-completion.
Publishes message to {@link CodeCompletionEnabledListener#TOPIC} */ -public class DisableCompletionsAction extends AnAction { +public class DisableCompletionsAction extends DumbAwareAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { diff --git a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java b/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java index 39fe132b..e74fe41f 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/EnableCompletionsAction.java @@ -1,15 +1,15 @@ package ee.carlrobert.codegpt.actions; -import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.DumbAwareAction; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import org.jetbrains.annotations.NotNull; /** * Enables code-completion.
Publishes message to {@link CodeCompletionEnabledListener#TOPIC} */ -public class EnableCompletionsAction extends AnAction { +public class EnableCompletionsAction extends DumbAwareAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { diff --git a/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.java b/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.java index d20028a5..f6576e88 100644 --- a/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.java +++ b/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionEventListener.java @@ -40,8 +40,7 @@ class CodeCompletionEventListener implements CompletionEventListener { progressIndicator.processFinish(); } - var editorManager = CodeGPTEditorManager.getInstance(); - editorManager.disposeEditorInlays(editor); + CodeGPTEditorManager.getInstance().disposeEditorInlays(editor); var inlayText = messageBuilder.toString(); if (!inlayText.isEmpty()) { @@ -60,7 +59,7 @@ class CodeCompletionEventListener implements CompletionEventListener { Notifications.Bus.notify(OverlayUtil.getDefaultNotification( String.format( CodeGPTBundle.get("notification.completionError.description"), - ex.getMessage()), + error.getMessage()), NotificationType.ERROR) .addAction(new OpenSettingsAction()), editor.getProject()); } diff --git a/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.java b/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.java index 0b376346..a982eb65 100644 --- a/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.java +++ b/src/main/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionService.java @@ -6,6 +6,7 @@ import static ee.carlrobert.codegpt.CodeGPTKeys.SINGLE_LINE_INLAY; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; +import com.intellij.codeInsight.lookup.LookupManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; @@ -24,13 +25,9 @@ import com.intellij.openapi.editor.InlayModel; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiClass; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiWhiteSpace; -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; import com.intellij.util.concurrency.annotations.RequiresEdt; import com.intellij.util.concurrency.annotations.RequiresReadLock; import com.intellij.util.concurrency.annotations.RequiresWriteLock; @@ -38,14 +35,12 @@ import ee.carlrobert.codegpt.actions.CodeCompletionEnabledListener; import ee.carlrobert.codegpt.completions.CompletionRequestService; import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import ee.carlrobert.codegpt.util.EditorUtil; -import ee.carlrobert.llm.completion.CompletionEventListener; import java.awt.event.KeyEvent; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.annotation.ParametersAreNonnullByDefault; import javax.swing.KeyStroke; -import okhttp3.sse.EventSource; import org.jetbrains.annotations.NotNull; @Service(PROJECT) @@ -53,7 +48,6 @@ import org.jetbrains.annotations.NotNull; public final class CodeCompletionService implements Disposable { public static final String APPLY_INLAY_ACTION_ID = "ApplyInlayAction"; - public static final int MAX_OFFSET = 4000; private static final Logger LOG = Logger.getInstance(CodeCompletionService.class); @@ -77,16 +71,13 @@ public final class CodeCompletionService implements Disposable { return project.getService(CodeCompletionService.class); } - public boolean isCompletionAllowed(PsiElement elementAtCaret) { - return elementAtCaret instanceof PsiWhiteSpace; - } - public void handleCompletions(Editor editor, int offset) { Project project = editor.getProject(); if (project == null || project.isDisposed() || !ConfigurationState.getInstance().isCodeCompletionsEnabled() || !EditorUtil.isSelectedEditor(editor) + || LookupManager.getActiveLookup(editor) != null || editor.isViewer() || editor.isOneLineMode() ) { @@ -99,54 +90,16 @@ public final class CodeCompletionService implements Disposable { return; } - PsiElement elementAtCaret = ReadAction.compute(() -> psiFile.findElementAt(offset)); - var completionService = CodeCompletionService.getInstance(project); - if (!completionService.isCompletionAllowed(elementAtCaret)) { - return; - } - + var request = InfillRequestDetails.fromDocumentWithMaxOffset(document, offset); callDebouncer.debounce( Void.class, - (progressIndicator) -> completionService.fetchCodeCompletion( - elementAtCaret, - offset, - document, + (progressIndicator) -> CompletionRequestService.getInstance().getCodeCompletionAsync( + request, new CodeCompletionEventListener(editor, offset, progressIndicator)), 500, TimeUnit.MILLISECONDS); } - /** - * Fetches code-completion (FIM) for the given position ({@code offsetInFile}) in the file.
- * By default tries to find an enclosing {@link PsiMethod} or {@link PsiClass} for the given - * {@code offsetInFile} and only uses their content instead of the entire file's content. If no - * such enclosing {@link PsiElement} can be found, the file's entire content is used instead. - * - * @param elementAtCaret PsiElement at caret - * @param offsetInFile Global offset in the file. - * @param document If the offset is not enclosed in a {@link PsiMethod} nor a - * {@link PsiClass}, the entire file content is used for completion. - * @return Completion String - */ - @RequiresBackgroundThread - public EventSource fetchCodeCompletion( - PsiElement elementAtCaret, - int offsetInFile, - Document document, - CompletionEventListener eventListener) { - InfillRequestDetails requestDetails = tryFindEnclosingPsiElementTextRange( - List.of(PsiMethod.class, PsiClass.class), elementAtCaret) - .map(textRange -> createInfillRequest( - document, - offsetInFile, - textRange.getStartOffset(), - textRange.getEndOffset()) - ) - .orElse(createInfillRequest(document, offsetInFile)); - return CompletionRequestService.getInstance() - .getCodeCompletionAsync(requestDetails, eventListener); - } - @RequiresEdt public void addInlays(Editor editor, int caretOffset, String inlayText) { List linesList = inlayText.lines().collect(toList()); @@ -201,11 +154,13 @@ public final class CodeCompletionService implements Disposable { Document document = editor.getDocument(); document.insertString(offset, text); editor.getCaretModel().moveToOffset(offset + text.length()); - EditorUtil.reformatDocument( - requireNonNull(editor.getProject()), - document, - offset, - offset + text.length()); + if (ConfigurationState.getInstance().isAutoFormattingEnabled()) { + EditorUtil.reformatDocument( + requireNonNull(editor.getProject()), + document, + offset, + offset + text.length()); + } } @RequiresReadLock @@ -246,20 +201,4 @@ public final class CodeCompletionService implements Disposable { APPLY_INLAY_ACTION_ID, new KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), null)); } - - private static InfillRequestDetails createInfillRequest(Document document, int offsetInFile) { - int begin = Integer.max(0, offsetInFile - MAX_OFFSET); - int end = Integer.min(document.getTextLength(), offsetInFile + MAX_OFFSET); - return createInfillRequest(document, offsetInFile, begin, end); - } - - private static InfillRequestDetails createInfillRequest( - Document document, - int caretOffset, - int start, - int end) { - return new InfillRequestDetails( - document.getText(new TextRange(start, caretOffset)), - document.getText(new TextRange(caretOffset, end))); - } } diff --git a/src/main/java/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.java b/src/main/java/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.java index de65554c..34e9cd73 100644 --- a/src/main/java/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.java +++ b/src/main/java/ee/carlrobert/codegpt/codecompletions/InfillRequestDetails.java @@ -1,7 +1,14 @@ package ee.carlrobert.codegpt.codecompletions; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import ee.carlrobert.codegpt.EncodingManager; + public class InfillRequestDetails { + private static final int MAX_OFFSET = 10_000; + private static final int MAX_PROMPT_TOKENS = 512; + private final String prefix; private final String suffix; @@ -10,6 +17,12 @@ public class InfillRequestDetails { this.suffix = suffix; } + public static InfillRequestDetails fromDocumentWithMaxOffset(Document document, int caretOffset) { + int start = Math.max(0, caretOffset - MAX_OFFSET); + int end = Math.min(document.getTextLength(), caretOffset + MAX_OFFSET); + return fromDocumentWithCustomRange(document, caretOffset, start, end); + } + public String getPrefix() { return prefix; } @@ -17,4 +30,21 @@ public class InfillRequestDetails { public String getSuffix() { return suffix; } + + private static InfillRequestDetails fromDocumentWithCustomRange( + Document document, + int caretOffset, + int start, + int end) { + var prefix = truncateText(document, start, caretOffset, false); + var suffix = truncateText(document, caretOffset, end, true); + return new InfillRequestDetails(prefix, suffix); + } + + private static String truncateText(Document document, int start, int end, boolean fromStart) { + return EncodingManager.getInstance().truncateText( + document.getText(new TextRange(start, end)), + MAX_PROMPT_TOKENS, + fromStart); + } } diff --git a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java index 87c46ac7..bb7c9799 100644 --- a/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java +++ b/src/test/java/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.java @@ -10,40 +10,29 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorCustomElementRenderer; -import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; +import com.intellij.testFramework.PlatformTestUtil; +import ee.carlrobert.codegpt.settings.configuration.ConfigurationState; import ee.carlrobert.llm.client.http.exchange.StreamHttpExchange; -import ee.carlrobert.llm.completion.CompletionEventListener; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import testsupport.IntegrationTest; public class CodeCompletionServiceTest extends IntegrationTest { - private final VisualPosition cursorPosition = new VisualPosition(2, 8); + private final VisualPosition cursorPosition = new VisualPosition(3, 0); public void testFetchCodeCompletionLlama() { useLlamaService(); - var codeCompletionService = CodeCompletionService.getInstance(getProject()); - String fileContents = getResourceContent( - "/codecompletions/code-completion-file.txt"); - PsiFile psiFile = myFixture.configureByText("CompletionTest.java", fileContents); + ConfigurationState.getInstance().setCodeCompletionsEnabled(true); + myFixture.configureByText( + "CompletionTest.java", + getResourceContent("/codecompletions/code-completion-file.txt")); Editor editor = myFixture.getEditor(); - Document document = editor.getDocument(); - editor.getCaretModel().moveToVisualPosition(cursorPosition); - var prefix = "public static int gcd(int x, int y){\n"; - var suffix = "\n" - + " }"; - var expectedCompletion = "return xyz;"; + var expectedCompletion = "TEST_SINGLE_LINE_OUTPUT\nTEST_MULTI_LINE_OUTPUT"; + var prefix = "z".repeat(1015) + "\n[INPUT]\n"; // 512 tokens + var suffix = "\n[\\INPUT]\n" + "z".repeat(1015); // 512 tokens expectLlama((StreamHttpExchange) request -> { assertThat(request.getUri().getPath()).isEqualTo("/completion"); assertThat(request.getMethod()).isEqualTo("POST"); @@ -53,86 +42,43 @@ public class CodeCompletionServiceTest extends IntegrationTest { return List.of(jsonMapResponse(e("content", expectedCompletion), e("stop", true))); }); - int caretOffset = editor.getCaretModel().getOffset(); - PsiElement elementAtCaret = ReadAction.compute(() -> psiFile.findElementAt(caretOffset)); + editor.getCaretModel().moveToVisualPosition(cursorPosition); - StringBuilder actualCompletion = new StringBuilder(); - codeCompletionService.fetchCodeCompletion(elementAtCaret, caretOffset, document, - new CompletionEventListener() { - @Override - public void onComplete(StringBuilder messageBuilder) { - actualCompletion.append(messageBuilder); + await().pollInSameThread().atMost(5, SECONDS) + .until(() -> { + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + var singleLineInlayElement = editor.getUserData(SINGLE_LINE_INLAY); + var multiLineInlayElement = editor.getUserData(MULTI_LINE_INLAY); + if (singleLineInlayElement != null && multiLineInlayElement != null) { + var singleLine = + ((InlayInlineElementRenderer) singleLineInlayElement.getRenderer()).getInlayText(); + var multiLine = + ((InlayBlockElementRenderer) multiLineInlayElement.getRenderer()).getInlayText(); + return "TEST_SINGLE_LINE_OUTPUT".equals(singleLine) + && "TEST_MULTI_LINE_OUTPUT".equals(multiLine); } + return false; }); - await().atMost(2, SECONDS) - .until(() -> actualCompletion.length() > 0); - assertEquals(expectedCompletion, actualCompletion.toString()); } - public void testAddInlaysSingleLine() { - var codeCompletionService = setupTestCodeCompletion(); - Editor editor = myFixture.getEditor(); + public void testApplyInlayAction() { + ConfigurationState.getInstance().setAutoFormattingEnabled(false); + myFixture.configureByText( + "CompletionTest.java", + getResourceContent("/codecompletions/code-completion-file.txt")); + var editor = myFixture.getEditor(); editor.getCaretModel().moveToVisualPosition(cursorPosition); - var expectedInlay = " return xyz;"; - int caretOffset = editor.getCaretModel().getOffset(); + var expectedSingleLineInlay = "FIRST_LINE"; + var expectedMultiLineInlay = "SECOND_LINE\nTHIRD_LINE"; + var expectedInlay = expectedSingleLineInlay + "\n" + expectedMultiLineInlay; + int cursorOffsetBeforeApply = editor.getCaretModel().getOffset(); + CodeCompletionService.getInstance(getProject()) + .addInlays(editor, cursorOffsetBeforeApply, expectedInlay); - codeCompletionService.addInlays(editor, caretOffset, expectedInlay); - - checkInlay(editor.getUserData(SINGLE_LINE_INLAY), InlayInlineElementRenderer.class, - expectedInlay, caretOffset); - checkPerformInlayAction(editor.getDocument(), cursorPosition.line, cursorPosition.line, - expectedInlay); - ActionManager.getInstance().unregisterAction(APPLY_INLAY_ACTION_ID); - } - - public void testAddInlaysMultiLine() { - var codeCompletionService = setupTestCodeCompletion(); - Editor editor = myFixture.getEditor(); - editor.getCaretModel().moveToVisualPosition(cursorPosition); - var expectedInlay = " int z = 1;\n z = 2 + 3;\n return xyz;"; - int caretOffset = editor.getCaretModel().getOffset(); - - codeCompletionService.addInlays(editor, caretOffset, expectedInlay); - - // First line of inlay - checkInlay(editor.getUserData(SINGLE_LINE_INLAY), InlayInlineElementRenderer.class, - expectedInlay.substring(0, expectedInlay.indexOf("\n")), caretOffset); - // Other lines of inlay - checkInlay(editor.getUserData(MULTI_LINE_INLAY), InlayBlockElementRenderer.class, - expectedInlay.substring(expectedInlay.indexOf("\n") + 1), caretOffset); - checkPerformInlayAction(editor.getDocument(), cursorPosition.line, cursorPosition.line + 2, - expectedInlay); - ActionManager.getInstance().unregisterAction(APPLY_INLAY_ACTION_ID); - } - - private CodeCompletionService setupTestCodeCompletion() { - useLlamaService(); - var codeCompletionService = CodeCompletionService.getInstance(getProject()); - String fileContents = getResourceContent( - "/codecompletions/code-completion-file.txt"); - myFixture.configureByText("CompletionTest.java", fileContents); - return codeCompletionService; - } - - private void checkInlay(Inlay inlay, - Class clazz, String expectedText, int expectedOffset) { - assertNotNull(inlay); - assertTrue(clazz.isInstance(inlay.getRenderer())); - InlayElementRenderer renderer = (InlayElementRenderer) inlay.getRenderer(); - assertEquals(expectedText, renderer.getInlayText()); - assertEquals(expectedOffset, inlay.getOffset()); - } - - private void checkPerformInlayAction(Document document, int startLine, int endLine, - String expectedText) { - AnAction applyInlayAction = ActionManager.getInstance().getAction(APPLY_INLAY_ACTION_ID); - assertNotNull(applyInlayAction); myFixture.performEditorAction(APPLY_INLAY_ACTION_ID); - TextRange inlayTextRange = new TextRange(document.getLineStartOffset(startLine), - document.getLineEndOffset(endLine)); - assertEquals(expectedText, document.getText(inlayTextRange)); + var newTextRange = new TextRange(cursorOffsetBeforeApply, editor.getCaretModel().getOffset()); + var appliedInlay = editor.getDocument().getText(newTextRange); + assertThat(appliedInlay).isEqualTo(expectedInlay); } - - } diff --git a/src/test/resources/codecompletions/code-completion-file.txt b/src/test/resources/codecompletions/code-completion-file.txt index 801cb3c1..d9fe637b 100644 --- a/src/test/resources/codecompletions/code-completion-file.txt +++ b/src/test/resources/codecompletions/code-completion-file.txt @@ -1,5 +1,7 @@ -public class CompletionTest { - public static int gcd(int x, int y){ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +[INPUT] - } -} \ No newline at end of file +[\INPUT] +zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file