Skip to content

Commit

Permalink
Merge pull request #630 from nipunayf/expression-editor-refactor
Browse files Browse the repository at this point in the history
Decouple the expression editor context from the flow node
  • Loading branch information
hasithaa authored Feb 27, 2025
2 parents 86388dc + 68fab3e commit d7a41c2
Show file tree
Hide file tree
Showing 45 changed files with 1,139 additions and 3,760 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@

package io.ballerina.flowmodelgenerator.core.expressioneditor;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.ballerina.compiler.syntax.tree.ImportDeclarationNode;
import io.ballerina.compiler.syntax.tree.ModulePartNode;
import io.ballerina.compiler.syntax.tree.SyntaxKind;
import io.ballerina.compiler.syntax.tree.SyntaxTree;
import io.ballerina.flowmodelgenerator.core.model.FlowNode;
import io.ballerina.flowmodelgenerator.core.model.NodeKind;
import io.ballerina.flowmodelgenerator.core.model.Property;
import io.ballerina.flowmodelgenerator.core.model.SourceBuilder;
import io.ballerina.modelgenerator.commons.CommonUtils;
import io.ballerina.projects.Document;
Expand Down Expand Up @@ -56,29 +53,25 @@
*/
public class ExpressionEditorContext {

private static final Gson gson = new Gson();
private final WorkspaceManagerProxy workspaceManagerProxy;
private final String fileUri;
private final Info info;
private final FlowNode flowNode;
private final Path filePath;
private final DocumentContext documentContext;
private final Property property;

// State variables
private int expressionOffset;
private LineRange statementLineRange;
private Property property;
private boolean propertyInitialized;

public ExpressionEditorContext(WorkspaceManagerProxy workspaceManagerProxy, String fileUri, Info info,
Path filePath) {
this.workspaceManagerProxy = workspaceManagerProxy;
this.fileUri = fileUri;
this.info = info;
this.filePath = filePath;
this.flowNode = gson.fromJson(info.node(), FlowNode.class);
this.propertyInitialized = false;
this.documentContext = new DocumentContext(workspaceManagerProxy, fileUri, filePath);
this.property = new Property(info.property(), info.codedata());
}

public ExpressionEditorContext(WorkspaceManagerProxy workspaceManagerProxy, String fileUri, Path filePath,
Expand All @@ -87,33 +80,13 @@ public ExpressionEditorContext(WorkspaceManagerProxy workspaceManagerProxy, Stri
this.fileUri = fileUri;
this.filePath = filePath;
this.info = null;
this.flowNode = null;
this.propertyInitialized = false;
this.documentContext = new DocumentContext(workspaceManagerProxy, fileUri, filePath, document);
}

public Property getProperty() {
if (propertyInitialized) {
return property;
}
if (info.property() == null || info.property().isEmpty()) {
this.property = null;
} else if (info.branch() == null || info.branch().isEmpty()) {
this.property = flowNode.getProperty(info.property()).orElse(null);
} else {
this.property = flowNode.getBranch(info.branch())
.flatMap(branch -> branch.getProperty(info.property()))
.orElse(null);
}
propertyInitialized = true;
return property;
this.property = new Property(null, null);
}

public boolean isNodeKind(List<NodeKind> nodeKinds) {
if (flowNode == null || flowNode.codedata() == null) {
return false;
}
return nodeKinds.contains(flowNode.codedata().node());
NodeKind nodeKind = property.nodeKind();
return nodeKind != null && nodeKinds.contains(nodeKind);
}

public LinePosition getStartLine() {
Expand All @@ -137,8 +110,8 @@ public Info info() {

// TODO: Check how we can use SourceBuilder in place of this method
private Optional<TextEdit> getImport() {
String org = flowNode.codedata().org();
String module = flowNode.codedata().module();
String org = property.org();
String module = property.module();

if (org == null || module == null || CommonUtils.isPredefinedLangLib(org, module)) {
return Optional.empty();
Expand Down Expand Up @@ -176,7 +149,7 @@ public Optional<TextEdit> getImport(String importStatement) {
.name(importStatement)
.endOfStatement()
.build(false);
TextEdit textEdit = TextEdit.from(TextRange.from(0, 0), stmt);
TextEdit textEdit = TextEdit.from(TextRange.from(0, 0), stmt + System.lineSeparator());
return Optional.of(textEdit);
}
return Optional.empty();
Expand All @@ -190,8 +163,8 @@ public Optional<TextEdit> getImport(String importStatement) {
*/
public LineRange generateStatement() {
String prefix = "var __reserved__ = ";
Property property = getProperty();
List<TextEdit> textEdits = new ArrayList<>();
int lineOffset = 0;

if (property != null) {
// Append the type if exists
Expand All @@ -200,11 +173,13 @@ public LineRange generateStatement() {
}

// Add the import statements of the dependent types
if (property.codedata() != null) {
String importStatements = property.codedata().importStatements();
if (importStatements != null && !importStatements.isEmpty()) {
for (String importStmt : importStatements.split(",")) {
getImport(importStmt).ifPresent(textEdits::add);
String importStatements = property.importStatements();
if (importStatements != null && !importStatements.isEmpty()) {
for (String importStmt : importStatements.split(",")) {
Optional<TextEdit> textEdit = getImport(importStmt);
if (textEdit.isPresent()) {
textEdits.add(textEdit.get());
lineOffset++;
}
}
}
Expand All @@ -213,12 +188,17 @@ public LineRange generateStatement() {
// Add the import statement for the node type
if (isNodeKind(List.of(NodeKind.NEW_CONNECTION, NodeKind.FUNCTION_CALL, NodeKind.REMOTE_ACTION_CALL,
NodeKind.RESOURCE_ACTION_CALL))) {
getImport().ifPresent(textEdits::add);
Optional<TextEdit> textEdit = getImport();
if (textEdit.isPresent()) {
textEdits.add(textEdit.get());
lineOffset++;
}
}

// Get the text position of the start line
TextDocument textDocument = documentContext.document().textDocument();
int textPosition = textDocument.textPositionFrom(info.startLine());
LinePosition cursorStartLine = info.startLine();
int textPosition = textDocument.textPositionFrom(cursorStartLine);

// Generate the statement and apply the text edits
String statement = String.format("%s%s;%n", prefix, info.expression());
Expand All @@ -227,7 +207,7 @@ public LineRange generateStatement() {
applyTextEdits(textEdits);

// Return the line range of the generated statement
LinePosition startLine = info.startLine();
LinePosition startLine = LinePosition.from(cursorStartLine.line() + lineOffset, cursorStartLine.offset());
LinePosition endLineRange = LinePosition.from(startLine.line(),
startLine.offset() + statement.length());
this.statementLineRange = LineRange.from(filePath.toString(), startLine, endLineRange);
Expand Down Expand Up @@ -292,18 +272,139 @@ public WorkspaceManager workspaceManager() {
return workspaceManagerProxy.get(fileUri);
}

public Property getProperty() {
return property;
}

/**
* Represents a property with associated code data in the expression editor context.
*
* <p>
* This is a temporary abstraction of the {@link io.ballerina.flowmodelgenerator.core.model.Property} and
* {@link io.ballerina.flowmodelgenerator.core.model.Codedata} until the source code is properly refactored to use
* these structures. The class provides the minimum public methods required to build the context and follows the CoR
* pattern to support fallback mechanisms for property derivation.
* </p>
* <p>
* The property encapsulates various attributes such as value, value type, type constraints, import statements,
* organization, module, and node kind. These values are lazily initialized when first accessed through the getter
* methods.
* </p>
*
* @since 2.0.0
*/
public static class Property {

private final JsonObject property;
private final JsonObject codedata;
private boolean initialized;

// Property values
private String value;
private String valueType;
private String typeConstraint;

// Codedata values
private String importStatements;
private String org;
private String module;
private NodeKind nodeKind;

private static final String VALUE_KEY = "value";
private static final String VALUE_TYPE_KEY = "valueType";
private static final String TYPE_CONSTRAINT_KEY = "valueTypeConstraint";
private static final String CODEDATA_KEY = "codedata";
private static final String IMPORT_STATEMENTS_KEY = "importStatements";
private static final String ORG_KEY = "org";
private static final String MODULE_KEY = "module";
private static final String NODE_KEY = "node";

public Property(JsonObject property, JsonObject codedata) {
this.property = property;
this.codedata = codedata;
this.initialized = false;
}

private void initialize() {
if (initialized) {
return;
}
if (property == null) {
value = "";
typeConstraint = "any";
} else {
value = property.has(VALUE_KEY) ? property.get(VALUE_KEY).getAsString() : "";
valueType = property.has(VALUE_TYPE_KEY) ? property.get(VALUE_TYPE_KEY).getAsString() : "";
typeConstraint =
property.has(TYPE_CONSTRAINT_KEY) ? property.get(TYPE_CONSTRAINT_KEY).getAsString() : "any";
JsonObject propertyCodedata =
property.has(CODEDATA_KEY) ? property.getAsJsonObject(CODEDATA_KEY) : null;
if (propertyCodedata != null) {
importStatements = propertyCodedata.has(IMPORT_STATEMENTS_KEY)
? propertyCodedata.get(IMPORT_STATEMENTS_KEY).getAsString() : "";
}
}

if (codedata == null) {
importStatements = "";
org = "";
module = "";
nodeKind = null;
} else {
org = codedata.has(ORG_KEY) ? codedata.get(ORG_KEY).getAsString() : "";
module = codedata.has(MODULE_KEY) ? codedata.get(MODULE_KEY).getAsString() : "";
nodeKind = codedata.has(NODE_KEY) ? NodeKind.valueOf(codedata.get(NODE_KEY).getAsString()) : null;
}
initialized = true;
}

public String value() {
initialize();
return value;
}

public String valueType() {
initialize();
return valueType;
}

public String valueTypeConstraint() {
initialize();
return typeConstraint;
}

public String importStatements() {
initialize();
return importStatements;
}

public String org() {
initialize();
return org;
}

public String module() {
initialize();
return module;
}

public NodeKind nodeKind() {
initialize();
return nodeKind;
}
}

/**
* Represents the json format of the expression editor context.
*
* @param expression The modified expression
* @param startLine The start line of the node
* @param offset The offset of cursor compared to the start of the expression
* @param node The node which contains the expression
* @param branch The branch of the expression if exists
* @param codedata The codedata of the expression
* @param property The property of the expression
*/
public record Info(String expression, LinePosition startLine, int offset, JsonObject node,
String branch, String property) {
public record Info(String expression, LinePosition startLine, int offset, JsonObject codedata,
JsonObject property) {
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public DiagnosticsRequest(ExpressionEditorContext context) {
}

public static DiagnosticsRequest from(ExpressionEditorContext context) {
Property property = context.getProperty();
ExpressionEditorContext.Property property = context.getProperty();
if (property == null) {
throw new IllegalArgumentException("Property cannot be null");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,7 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
}

@JsonRequest
public CompletableFuture<DiagnosticsRequest.Diagnostics> diagnostics(
ExpressionEditorDiagnosticsRequest request) {
public CompletableFuture<DiagnosticsRequest.Diagnostics> diagnostics(ExpressionEditorDiagnosticsRequest request) {
String fileUri = CommonUtils.getExprUri(request.filePath());
return Debouncer.getInstance().debounce(DiagnosticsRequest.from(
new ExpressionEditorContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void testMultipleRequests() throws IOException {
ExpressionEditorContext.Info firstContext = testConfig.context();
ExpressionEditorContext.Info info =
new ExpressionEditorContext.Info("self.classVar > localVar + self. + 21", firstContext.startLine(),
32, firstContext.node(), firstContext.branch(), firstContext.property());
32, firstContext.codedata(), firstContext.property());
ExpressionEditorCompletionRequest secondRequest = new ExpressionEditorCompletionRequest(sourcePath, info,
testConfig.completionContext());
JsonObject secondResponse = getResponse(secondRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ public void testMultipleRequests() throws IOException, InterruptedException {
};
for (int i = 0; i < expressionSteps.length - 1; i++) {
ExpressionEditorContext.Info context = new ExpressionEditorContext.Info(expressionSteps[i],
templateContext.startLine(), templateContext.offset(), templateContext.node(),
templateContext.branch(), templateContext.property());
templateContext.startLine(), templateContext.offset(), templateContext.codedata(),
templateContext.property());
ExpressionEditorDiagnosticsRequest req = new ExpressionEditorDiagnosticsRequest(sourcePath, context);
serviceEndpoint.request(method, req);
Thread.sleep(400);
Expand All @@ -96,7 +96,7 @@ public void testMultipleRequests() throws IOException, InterruptedException {
// In the final complete expression, assert that no diagnostics are returned
ExpressionEditorContext.Info context =
new ExpressionEditorContext.Info("fn({id: 0})", templateContext.startLine(), templateContext.offset(),
templateContext.node(), templateContext.branch(), templateContext.property());
templateContext.codedata(), templateContext.property());
ExpressionEditorDiagnosticsRequest req = new ExpressionEditorDiagnosticsRequest(sourcePath, context);
JsonObject resp = getResponse(req);
List<Diagnostic> diagnostics = gson.fromJson(resp.get("diagnostics").getAsJsonArray(), diagnosticsType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ public void testDiagnostics(Path config) throws IOException {
template = template.replace("${1}", " ");
}
ExpressionEditorContext.Info info = new ExpressionEditorContext.Info(template, startPosition, offset,
variableNode, null, "expression");
variableNode.get("codedata").getAsJsonObject(),
variableNode.getAsJsonObject("properties").getAsJsonObject("expression"));
ExpressionEditorDiagnosticsRequest diagnosticsRequest =
new ExpressionEditorDiagnosticsRequest(sourcePath, info);
JsonObject response = getResponse(diagnosticsRequest, "expressionEditor/diagnostics");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public void test(Path config) throws IOException {
// Send diagnostics request
ExpressionEditorContext.Info info =
new ExpressionEditorContext.Info(testConfig.expression(), LinePosition.from(1, 0),
0, variableNode, null, "expression");
0, variableNode.get("codedata").getAsJsonObject(),
variableNode.getAsJsonObject("properties").getAsJsonObject("expression"));
ExpressionEditorDiagnosticsRequest diagnosticsRequest =
new ExpressionEditorDiagnosticsRequest(sourcePath, info);
JsonObject response = getResponse(diagnosticsRequest, "expressionEditor/diagnostics");
Expand Down
Loading

0 comments on commit d7a41c2

Please sign in to comment.