Skip to content

Commit

Permalink
Merge pull request #474 from nipunayf/expression-editor-diagnostics
Browse files Browse the repository at this point in the history
Introduce a new API to retrieve diagnostics for the inline expression editor
  • Loading branch information
hasithaa authored Nov 7, 2024
2 parents 77d2d5d + 5d119f2 commit 634c0c4
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import io.ballerina.tools.text.TextRange;
import org.ballerinalang.langserver.common.utils.CommonUtil;
import org.ballerinalang.langserver.commons.workspace.WorkspaceManager;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

Expand Down Expand Up @@ -361,6 +363,7 @@ public static boolean withinDoClause(WorkspaceManager workspaceManager, Path fil

/**
* Check whether the given node is within a do clause.
*
* @param node the node to check
* @return true if the node is within a do clause, false otherwise
*/
Expand Down Expand Up @@ -388,6 +391,7 @@ public static String generateIcon(String orgName, String packageName, String ver

/**
* Builds the resource path template for the given function symbol.
*
* @param functionSymbol the function symbol
* @return the resource path template
*/
Expand Down Expand Up @@ -428,6 +432,7 @@ public static String buildResourcePathTemplate(FunctionSymbol functionSymbol) {

/**
* Check whether the given type is a subtype of the target type.
*
* @param source the source type
* @param target the target type
* @return true if the source type is a subtype of the target type, false otherwise
Expand Down Expand Up @@ -456,4 +461,37 @@ public static boolean subTypeOf(TypeSymbol source, TypeSymbol target) {
}
return sourceRawType.subtypeOf(target);
}

//TODO: Remove this once the diagnostic helper is exposed to LS extensions
public static Diagnostic transformBallerinaDiagnostic(io.ballerina.tools.diagnostics.Diagnostic diag) {
LineRange lineRange = diag.location().lineRange();
int startLine = lineRange.startLine().line();
int startChar = lineRange.startLine().offset();
int endLine = lineRange.endLine().line();
int endChar = lineRange.endLine().offset();

endLine = (endLine <= 0) ? startLine : endLine;
endChar = (endChar <= 0) ? startChar + 1 : endChar;

Range range = new Range(new Position(startLine, startChar), new Position(endLine, endChar));
Diagnostic diagnostic = new Diagnostic(range, diag.message(), null, null, diag.diagnosticInfo().code());

switch (diag.diagnosticInfo().severity()) {
case ERROR:
diagnostic.setSeverity(DiagnosticSeverity.Error);
break;
case WARNING:
diagnostic.setSeverity(DiagnosticSeverity.Warning);
break;
case HINT:
diagnostic.setSeverity(DiagnosticSeverity.Hint);
break;
case INFO:
diagnostic.setSeverity(DiagnosticSeverity.Information);
break;
default:
break;
}
return diagnostic;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,32 @@

import com.google.gson.JsonArray;
import io.ballerina.compiler.api.SemanticModel;
import io.ballerina.flowmodelgenerator.core.CommonUtils;
import io.ballerina.flowmodelgenerator.core.TypesGenerator;
import io.ballerina.flowmodelgenerator.core.VisibleVariableTypesGenerator;
import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorCompletionRequest;
import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorDiagnosticsRequest;
import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorSignatureRequest;
import io.ballerina.flowmodelgenerator.extension.request.VisibleVariableTypeRequest;
import io.ballerina.flowmodelgenerator.extension.response.ExpressionEditorDiagnosticsResponse;
import io.ballerina.flowmodelgenerator.extension.response.ExpressionEditorTypeResponse;
import io.ballerina.flowmodelgenerator.extension.response.VisibleVariableTypesResponse;
import io.ballerina.projects.Document;
import io.ballerina.projects.Module;
import io.ballerina.tools.text.LinePosition;
import io.ballerina.tools.text.LineRange;
import io.ballerina.tools.text.TextDocument;
import io.ballerina.tools.text.TextDocumentChange;
import io.ballerina.tools.text.TextEdit;
import io.ballerina.tools.text.TextRange;
import org.ballerinalang.annotation.JavaSPIService;
import org.ballerinalang.langserver.common.utils.PositionUtil;
import org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService;
import org.ballerinalang.langserver.commons.workspace.WorkspaceManager;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.FileChangeType;
import org.eclipse.lsp4j.FileEvent;
Expand Down Expand Up @@ -242,4 +250,79 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
}
});
}

@JsonRequest
public CompletableFuture<ExpressionEditorDiagnosticsResponse> diagnostics(
ExpressionEditorDiagnosticsRequest request) {
return CompletableFuture.supplyAsync(() -> {
ExpressionEditorDiagnosticsResponse response = new ExpressionEditorDiagnosticsResponse();
Path projectPath = null;
try {
// Load the original project
Path filePath = Path.of(request.filePath());
this.workspaceManager.loadProject(filePath);
projectPath = this.workspaceManager.projectRoot(filePath);

// Load the shadowed project
ProjectCacheManager projectCacheManager =
ProjectCacheManager.InstanceHandler.getInstance(projectPath);
projectCacheManager.copyContent();
Path destination = projectCacheManager.getDestination(filePath);

FileEvent fileEvent = new FileEvent(destination.toUri().toString(), FileChangeType.Changed);
DidChangeWatchedFilesParams didChangeWatchedFilesParams =
new DidChangeWatchedFilesParams(List.of(fileEvent));
this.langServer.getWorkspaceService().didChangeWatchedFiles(didChangeWatchedFilesParams);
this.workspaceManager.loadProject(destination);

// Get the document
Optional<Document> document = this.workspaceManager.document(destination);
if (document.isEmpty()) {
return response;
}
TextDocument textDocument = document.get().textDocument();

// Determine the cursor position
int textPosition = textDocument.textPositionFrom(request.startLine());

String type = request.type();
String statement;
if (type == null || type.isEmpty()) {
statement = String.format("_ = %s;%n", request.expression());
} else {
statement = String.format("%s _ = %s;%n", type, request.expression());
}
LinePosition endLineRange = LinePosition.from(request.startLine().line(),
request.startLine().offset() + statement.length());
LineRange lineRange = LineRange.from(request.filePath(), request.startLine(), endLineRange);

TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), statement);
TextDocument newTextDocument =
textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0])));
projectCacheManager.writeContent(newTextDocument, filePath);
document.get().modify()
.withContent(String.join(System.lineSeparator(), newTextDocument.textLines()))
.apply();

Optional<Module> module = workspaceManager.module(destination);
if (module.isEmpty()) {
return response;
}
List<Diagnostic> diagnostics = module.get().getCompilation().diagnostics().diagnostics().stream()
.filter(diagnostic -> PositionUtil.isWithinLineRange(diagnostic.location().lineRange(),
lineRange))
.map(CommonUtils::transformBallerinaDiagnostic)
.toList();
projectCacheManager.deleteContent();
response.setDiagnostics(diagnostics);
} catch (Throwable e) {
response.setError(e);
} finally {
if (projectPath != null) {
ProjectCacheManager.InstanceHandler.release(projectPath);
}
}
return response;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com)
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package io.ballerina.flowmodelgenerator.extension.request;

import io.ballerina.tools.text.LinePosition;

/**
* Represents a request for diagnostics in the expression editor.
*
* @param filePath the path of the file
* @param expression the value in the expression field
* @param type the type of the expression
* @param startLine the starting line position of the expression
*/
public record ExpressionEditorDiagnosticsRequest(String filePath, String expression, String type,
LinePosition startLine) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com)
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package io.ballerina.flowmodelgenerator.extension.response;

import org.eclipse.lsp4j.Diagnostic;

import java.util.List;

/**
* This class represents the response containing diagnostics for the expression editor.
*
* @since 1.4.0
*/
public class ExpressionEditorDiagnosticsResponse extends AbstractFlowModelResponse {

List<Diagnostic> diagnostics;

public List<Diagnostic> diagnostics() {
return diagnostics;
}

public void setDiagnostics(List<Diagnostic> diagnostics) {
this.diagnostics = diagnostics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com)
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package io.ballerina.flowmodelgenerator.extension;

import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorDiagnosticsRequest;
import io.ballerina.tools.text.LinePosition;
import org.eclipse.lsp4j.Diagnostic;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

/**
* Tests for the expression editor diagnostics service.
*
* @since 1.4.0
*/
public class ExpressionEditorDiagnosticsTest extends AbstractLSTest {

@Override
@Test(dataProvider = "data-provider")
public void test(Path config) throws IOException {
Path configJsonPath = configDir.resolve(config);
TestConfig testConfig = gson.fromJson(Files.newBufferedReader(configJsonPath), TestConfig.class);

ExpressionEditorDiagnosticsRequest request =
new ExpressionEditorDiagnosticsRequest(getSourcePath(testConfig.filePath()), testConfig.expression(),
testConfig.type(), testConfig.startLine());
JsonObject response = getResponse(request);

List<Diagnostic> actualDiagnostics = gson.fromJson(response.get("diagnostics").getAsJsonArray(),
new TypeToken<List<Diagnostic>>() { }.getType());
if (!assertArray("diagnostics", actualDiagnostics, testConfig.diagnostics())) {
TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.filePath(),
testConfig.expression(), testConfig.startLine(), testConfig.type(), actualDiagnostics);
updateConfig(configJsonPath, updatedConfig);
Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath));
}
}

@DataProvider(name = "data-provider")
@Override
protected Object[] getConfigsList() {
return new Object[]{
Path.of("single5.json")
};
}

@Override
protected String getResourceDir() {
return "diagnostics";
}

@Override
protected Class<? extends AbstractLSTest> clazz() {
return ExpressionEditorDiagnosticsTest.class;
}

@Override
protected String getApiName() {
return "diagnostics";
}

@Override
protected String getServiceName() {
return "expressionEditor";
}

private record TestConfig(String description, String filePath, String expression, LinePosition startLine,
String type, List<Diagnostic> diagnostics) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"description": "",
"filePath": "source.bal",
"expression": "self.classVar > localVar + 12",
"startLine": {
"line": 13,
"offset": 8
},
"type": "boolean",
"diagnostics": [
{
"range": {
"start": {
"line": 13,
"character": 36
},
"end": {
"line": 13,
"character": 50
}
},
"severity": "Error",
"code": {
"left": "BCE2070"
},
"message": "operator '+' not defined for 'float' and 'int'"
}
]
}
Loading

0 comments on commit 634c0c4

Please sign in to comment.