Skip to content

Commit

Permalink
Added Java-specific unit tests for server-side on-type formatting.
Browse files Browse the repository at this point in the history
  • Loading branch information
SCWells72 committed Jan 20, 2025
1 parent dae0e52 commit 57a0612
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package com.redhat.devtools.lsp4ij.features.formatting;

import com.redhat.devtools.lsp4ij.fixtures.LSPServerSideOnTypeFormattingFixtureTestCase;

/**
* Java-based server-side on-type formatting tests for format-on-close brace. Note that jdtls' support for on-type
* formatting does not seem to work well enough for format-on-statement terminator or newline even though those are
* included as supported on-type formatting characters in the language server initialization response. Both of those
* characters seem to yield empty text edit lists to be applied.
*/
public class JavaClientSideFormatOnCloseBraceTest extends LSPServerSideOnTypeFormattingFixtureTestCase {

private static final String TEST_FILE_NAME = "Test.java";

public JavaClientSideFormatOnCloseBraceTest() {
super("*.java");
// On-type formatting trigger characters for jdtls
setTriggerCharacters(";", "\n", "}");
}

public void testSimple() {
assertOnTypeFormatting(
TEST_FILE_NAME,
// No language injection here because there are syntax errors
"""
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world.");
// type }
}
""",
// language=java
"""
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world.");
}
}
""",
// language=json
"""
[
{
"range": {
"start": {
"line": 1,
"character": 44
},
"end": {
"line": 2,
"character": 0
}
},
"newText": "\\n "
}
]
"""
);
}

public void testComplex() {
assertOnTypeFormatting(
TEST_FILE_NAME,
// No language injection here because there are syntax errors
"""
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world.");
}
// type }
""",
// language=java
"""
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world.");
}
}
""",
// language=json
"""
[
{
"range": {
"start": {
"line": 0,
"character": 20
},
"end": {
"line": 1,
"character": 0
}
},
"newText": "\\n "
},
{
"range": {
"start": {
"line": 1,
"character": 40
},
"end": {
"line": 2,
"character": 0
}
},
"newText": "\\n "
},
{
"range": {
"start": {
"line": 2,
"character": 36
},
"end": {
"line": 3,
"character": 0
}
},
"newText": "\\n "
}
]
"""
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package com.redhat.devtools.lsp4ij.fixtures;

import com.google.gson.reflect.TypeToken;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.EditorTestUtil;
import com.intellij.util.containers.ContainerUtil;
import com.redhat.devtools.lsp4ij.JSONUtils;
import com.redhat.devtools.lsp4ij.LanguageServerItem;
import com.redhat.devtools.lsp4ij.LanguageServiceAccessor;
import com.redhat.devtools.lsp4ij.mock.MockLanguageServer;
import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions;
import org.eclipse.lsp4j.TextEdit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Base class test case for server-side on-type formatting tests by emulating LSP 'textDocument/onTypeFormatting' responses.
*/
public abstract class LSPServerSideOnTypeFormattingFixtureTestCase extends LSPCodeInsightFixtureTestCase {

private static final Pattern TYPE_INFO_PATTERN = Pattern.compile("(?ms)//\\s*type\\s*(\\S)[\\t ]*");

private List<String> triggerCharacters = new LinkedList<>();

public LSPServerSideOnTypeFormattingFixtureTestCase(String... fileNamePatterns) {
super(fileNamePatterns);
}

protected void setTriggerCharacters(@NotNull String... triggerCharacters) {
ContainerUtil.addAllNotNull(this.triggerCharacters, triggerCharacters);
}

/**
* Asserts that <code>fileBodyBefore</code> is formatted into <code>fileBodyAfter</code> when the typing imperative
* embedded in <code>fileBodyBefore</code> is applied. The typing imperative is just a simple comment of the form
* <code>// type &lt;character&gt;</code> at the offset at which <code>&lt;character&gt;</code> should be typed.
* A mock LSP response must be provided for <code>textDocument/onTypeFormatting</code>.
*
* @param fileName the file name
* @param fileBodyBefore the file body before including the embedded typing imperative comment
* @param fileBodyAfter the file body after the character has been typed and formatting applied
* @param mockOnTypeFormattingJson the mock on-type formatting JSON response
*/
protected void assertOnTypeFormatting(@NotNull String fileName,
@NotNull String fileBodyBefore,
@NotNull String fileBodyAfter,
@NotNull String mockOnTypeFormattingJson) {
MockLanguageServer.INSTANCE.setTimeToProceedQueries(100);

List<TextEdit> mockTextEdits = JSONUtils.getLsp4jGson().fromJson(mockOnTypeFormattingJson, new TypeToken<List<TextEdit>>() {
}.getType());
MockLanguageServer.INSTANCE.setFormattingTextEdits(mockTextEdits);

Project project = myFixture.getProject();
PsiFile file = myFixture.configureByText(fileName, removeTypeInfo(fileBodyBefore));
Editor editor = myFixture.getEditor();

// Initialize the language server
List<LanguageServerItem> languageServers = new LinkedList<>();
try {
ContainerUtil.addAllNotNull(languageServers, LanguageServiceAccessor.getInstance(project)
.getLanguageServers(file.getVirtualFile(), null, null)
.get(5000, TimeUnit.MILLISECONDS));
} catch (Exception e) {
fail(e.getMessage());
}

// Configure the language server for client-side on-type formatting
LanguageServerItem languageServer = ContainerUtil.getFirstItem(languageServers);
assertNotNull(languageServer);

// Enable on-type formatting with the specified trigger characters
String firstTriggerCharacter = ContainerUtil.getFirstItem(triggerCharacters);
assertNotNull("At least one trigger character must be specified.", firstTriggerCharacter);
List<String> moreTriggerCharacters = triggerCharacters.size() > 1 ? triggerCharacters.subList(1, triggerCharacters.size()) : Collections.emptyList();
languageServer.getServerCapabilities().setDocumentOnTypeFormattingProvider(new DocumentOnTypeFormattingOptions(firstTriggerCharacter, moreTriggerCharacters));

EditorTestUtil.buildInitialFoldingsInBackground(editor);

// Derive the offset and character that should be typed
Pair<Integer, Character> typeInfo = getTypeInfo(fileBodyBefore);
assertNotNull("No type information found in file body before.", typeInfo);
int offset = typeInfo.getFirst();
char character = typeInfo.getSecond();

// Move to the offset and type the character
CaretModel caretModel = editor.getCaretModel();
caretModel.moveToOffset(offset);
EditorTestUtil.performTypingAction(editor, character);

// Confirm that the file body has been reformatted as expected
assertEquals(fileBodyAfter, editor.getDocument().getText());
}

@NotNull
private String removeTypeInfo(@NotNull String fileBodyBefore) {
return TYPE_INFO_PATTERN.matcher(fileBodyBefore).replaceFirst("");
}

@Nullable
private Pair<Integer, Character> getTypeInfo(@NotNull String fileBodyBefore) {
Matcher typeInfoMatcher = TYPE_INFO_PATTERN.matcher(fileBodyBefore);
if (typeInfoMatcher.find()) {
int offset = typeInfoMatcher.start();
char character = typeInfoMatcher.group(1).charAt(0);
return Pair.create(offset, character);
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRange

@Override
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) {
return CompletableFuture.completedFuture(null);
return CompletableFuture.completedFuture(mockFormattingTextEdits);
}

@Override
Expand Down

0 comments on commit 57a0612

Please sign in to comment.