Revert "feat: code completion improvements"

This reverts commit abc8dc8d07.
This commit is contained in:
Carl-Robert Linnupuu 2024-02-05 16:28:18 +02:00
parent abc8dc8d07
commit 7f586da0c1
8 changed files with 177 additions and 109 deletions

View file

@ -9,7 +9,6 @@ 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 {
@ -53,19 +52,4 @@ 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<Integer> 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));
}
}

View file

@ -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.<br/> Publishes message to {@link CodeCompletionEnabledListener#TOPIC}
*/
public class DisableCompletionsAction extends DumbAwareAction {
public class DisableCompletionsAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {

View file

@ -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.<br/> Publishes message to {@link CodeCompletionEnabledListener#TOPIC}
*/
public class EnableCompletionsAction extends DumbAwareAction {
public class EnableCompletionsAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {

View file

@ -40,7 +40,8 @@ class CodeCompletionEventListener implements CompletionEventListener {
progressIndicator.processFinish();
}
CodeGPTEditorManager.getInstance().disposeEditorInlays(editor);
var editorManager = CodeGPTEditorManager.getInstance();
editorManager.disposeEditorInlays(editor);
var inlayText = messageBuilder.toString();
if (!inlayText.isEmpty()) {
@ -59,7 +60,7 @@ class CodeCompletionEventListener implements CompletionEventListener {
Notifications.Bus.notify(OverlayUtil.getDefaultNotification(
String.format(
CodeGPTBundle.get("notification.completionError.description"),
error.getMessage()),
ex.getMessage()),
NotificationType.ERROR)
.addAction(new OpenSettingsAction()), editor.getProject());
}

View file

@ -6,7 +6,6 @@ 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;
@ -25,9 +24,13 @@ 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;
@ -35,12 +38,14 @@ 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)
@ -48,6 +53,7 @@ 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);
@ -71,13 +77,16 @@ 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()
) {
@ -90,16 +99,54 @@ public final class CodeCompletionService implements Disposable {
return;
}
var request = InfillRequestDetails.fromDocumentWithMaxOffset(document, offset);
PsiElement elementAtCaret = ReadAction.compute(() -> psiFile.findElementAt(offset));
var completionService = CodeCompletionService.getInstance(project);
if (!completionService.isCompletionAllowed(elementAtCaret)) {
return;
}
callDebouncer.debounce(
Void.class,
(progressIndicator) -> CompletionRequestService.getInstance().getCodeCompletionAsync(
request,
(progressIndicator) -> completionService.fetchCodeCompletion(
elementAtCaret,
offset,
document,
new CodeCompletionEventListener(editor, offset, progressIndicator)),
500,
TimeUnit.MILLISECONDS);
}
/**
* Fetches code-completion (FIM) for the given position ({@code offsetInFile}) in the file. <br/>
* 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<String> linesList = inlayText.lines().collect(toList());
@ -154,13 +201,11 @@ public final class CodeCompletionService implements Disposable {
Document document = editor.getDocument();
document.insertString(offset, text);
editor.getCaretModel().moveToOffset(offset + text.length());
if (ConfigurationState.getInstance().isAutoFormattingEnabled()) {
EditorUtil.reformatDocument(
requireNonNull(editor.getProject()),
document,
offset,
offset + text.length());
}
EditorUtil.reformatDocument(
requireNonNull(editor.getProject()),
document,
offset,
offset + text.length());
}
@RequiresReadLock
@ -201,4 +246,20 @@ 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)));
}
}

View file

@ -1,14 +1,7 @@
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;
@ -17,12 +10,6 @@ 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;
}
@ -30,21 +17,4 @@ 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);
}
}

View file

@ -10,29 +10,40 @@ 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.testFramework.PlatformTestUtil;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
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(3, 0);
private final VisualPosition cursorPosition = new VisualPosition(2, 8);
public void testFetchCodeCompletionLlama() {
useLlamaService();
ConfigurationState.getInstance().setCodeCompletionsEnabled(true);
myFixture.configureByText(
"CompletionTest.java",
getResourceContent("/codecompletions/code-completion-file.txt"));
var codeCompletionService = CodeCompletionService.getInstance(getProject());
String fileContents = getResourceContent(
"/codecompletions/code-completion-file.txt");
PsiFile psiFile = myFixture.configureByText("CompletionTest.java", fileContents);
Editor editor = myFixture.getEditor();
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
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;";
expectLlama((StreamHttpExchange) request -> {
assertThat(request.getUri().getPath()).isEqualTo("/completion");
assertThat(request.getMethod()).isEqualTo("POST");
@ -42,43 +53,86 @@ public class CodeCompletionServiceTest extends IntegrationTest {
return List.of(jsonMapResponse(e("content", expectedCompletion), e("stop", true)));
});
editor.getCaretModel().moveToVisualPosition(cursorPosition);
int caretOffset = editor.getCaretModel().getOffset();
PsiElement elementAtCaret = ReadAction.compute(() -> psiFile.findElementAt(caretOffset));
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);
StringBuilder actualCompletion = new StringBuilder();
codeCompletionService.fetchCodeCompletion(elementAtCaret, caretOffset, document,
new CompletionEventListener() {
@Override
public void onComplete(StringBuilder messageBuilder) {
actualCompletion.append(messageBuilder);
}
return false;
});
await().atMost(2, SECONDS)
.until(() -> actualCompletion.length() > 0);
assertEquals(expectedCompletion, actualCompletion.toString());
}
public void testApplyInlayAction() {
ConfigurationState.getInstance().setAutoFormattingEnabled(false);
myFixture.configureByText(
"CompletionTest.java",
getResourceContent("/codecompletions/code-completion-file.txt"));
var editor = myFixture.getEditor();
public void testAddInlaysSingleLine() {
var codeCompletionService = setupTestCodeCompletion();
Editor editor = myFixture.getEditor();
editor.getCaretModel().moveToVisualPosition(cursorPosition);
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);
var expectedInlay = " return xyz;";
int caretOffset = editor.getCaretModel().getOffset();
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<EditorCustomElementRenderer> inlay,
Class<? extends EditorCustomElementRenderer> 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);
var newTextRange = new TextRange(cursorOffsetBeforeApply, editor.getCaretModel().getOffset());
var appliedInlay = editor.getDocument().getText(newTextRange);
assertThat(appliedInlay).isEqualTo(expectedInlay);
TextRange inlayTextRange = new TextRange(document.getLineStartOffset(startLine),
document.getLineEndOffset(endLine));
assertEquals(expectedText, document.getText(inlayTextRange));
}
}

View file

@ -1,7 +1,5 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
[INPUT]
public class CompletionTest {
public static int gcd(int x, int y){
[\INPUT]
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
}
}