Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide completions for expressions in the expression editor #403

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorCompletionRequest;
import io.ballerina.projects.Document;
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.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.Position;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
import org.eclipse.lsp4j.services.LanguageServer;

import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

@JavaSPIService("org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService")
@JsonSegment("expressionEditor")
public class ExpressionEditorService implements ExtendedLanguageServerService {

private WorkspaceManager workspaceManager;
private LanguageServer langServer;

@Override
public void init(LanguageServer langServer, WorkspaceManager workspaceManager) {
this.workspaceManager = workspaceManager;
this.langServer = langServer;
}

@Override
public Class<?> getRemoteInterface() {
return null;
}

@JsonRequest
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(
ExpressionEditorCompletionRequest request) {
return CompletableFuture.supplyAsync(() -> {
try {
// Load the project
Path filePath = Path.of(request.filePath());
this.workspaceManager.loadProject(filePath);
Path projectPath = this.workspaceManager.projectRoot(filePath);

// Create a temporary directory and load the project
ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath);
projectCacheManager.createTempDirectory();
Path destination = projectCacheManager.getDestination();
this.workspaceManager.loadProject(destination);

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

// Determine the cursor position
int textPosition = textDocument.textPositionFrom(request.startLine());
String statement = String.format("_ = %s;%n", request.expression());
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);
document.get().modify()
.withContent(String.join(System.lineSeparator(), newTextDocument.textLines()))
.apply();

// Generate the completion params
Position position =
new Position(request.startLine().line(), request.startLine().offset() + 4 + request.offset());
TextDocumentIdentifier identifier = new TextDocumentIdentifier(destination.toUri().toString());
CompletionParams params = new CompletionParams(identifier, position, request.context());

// Get the completions
CompletableFuture<Either<List<CompletionItem>, CompletionList>> completableFuture =
langServer.getTextDocumentService().completion(params);
Either<List<CompletionItem>, CompletionList> completions = completableFuture.join();
projectCacheManager.deleteCache();
return completions;
} catch (Throwable e) {
return Either.forLeft(List.of());
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
import io.ballerina.flowmodelgenerator.extension.response.FlowModelSourceGeneratorResponse;
import io.ballerina.flowmodelgenerator.extension.response.FlowNodeDeleteResponse;
import io.ballerina.projects.Document;
import io.ballerina.projects.DocumentId;
import io.ballerina.projects.Module;
import io.ballerina.projects.Project;
import io.ballerina.tools.text.LinePosition;
import io.ballerina.tools.text.LineRange;
Expand All @@ -62,15 +64,11 @@
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
import org.eclipse.lsp4j.services.LanguageServer;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

/**
* Represents the extended language server service for the flow model generator service.
Expand Down Expand Up @@ -138,7 +136,7 @@ public CompletableFuture<FlowModelGeneratorResponse> getSuggestedFlowModel(
Path filePath = Path.of(request.filePath());

// Obtain the semantic model and the document
this.workspaceManager.loadProject(filePath);
Project project = this.workspaceManager.loadProject(filePath);
Optional<SemanticModel> semanticModel = this.workspaceManager.semanticModel(filePath);
Optional<Document> document = this.workspaceManager.document(filePath);
if (semanticModel.isEmpty() || document.isEmpty()) {
Expand All @@ -159,48 +157,31 @@ public CompletableFuture<FlowModelGeneratorResponse> getSuggestedFlowModel(
JsonElement oldFlowModel = modelGenerator.getFlowModel();

// Create a temporary directory for the in-memory cache
Path tempDir = Files.createTempDirectory("project-cache");
Path destinationDir = tempDir.resolve(projectPath.getFileName());

if (Files.isDirectory(projectPath)) {
try (Stream<Path> paths = Files.walk(projectPath)) {
paths.forEach(source -> {
try {
Files.copy(source, destinationDir.resolve(projectPath.relativize(source)),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to copy project directory to cache", e);
}
});
} catch (IOException e) {
throw new RuntimeException("Failed to walk project directory", e);
}
} else {
Files.copy(projectPath, destinationDir, StandardCopyOption.REPLACE_EXISTING);
}

Path destination = destinationDir.resolve(projectPath.relativize(projectPath.resolve(filePath)));
Project newProject = this.workspaceManager.loadProject(destination);
Optional<SemanticModel> newSemanticModel = this.workspaceManager.semanticModel(destination);
Optional<Document> newDocument = this.workspaceManager.document(destination);
if (newSemanticModel.isEmpty() || newDocument.isEmpty()) {
Project newProject = project.duplicate();
DocumentId documentId = project.documentId(filePath);
Module newModule = project.currentPackage().module(documentId.moduleId());
SemanticModel newSemanticModel =
newProject.currentPackage().getCompilation().getSemanticModel(newModule.moduleId());
Document newDocument = newModule.document(documentId);
if (newSemanticModel == null || newDocument == null) {
return response;
}
Path newProjectPath = this.workspaceManager.projectRoot(destination);
Optional<Document> newDataMappingsDoc;
try {
newDataMappingsDoc = this.workspaceManager.document(newProjectPath.resolve("data_mappings.bal"));
DocumentId dataMappingDocId = newProject.documentId(projectPath.resolve("data_mappings.bal"));
Module dataMappingModule = newProject.currentPackage().module(dataMappingDocId.moduleId());
newDataMappingsDoc = Optional.of(dataMappingModule.document(dataMappingDocId));
} catch (Throwable e) {
newDataMappingsDoc = Optional.empty();
}

TextDocument textDocument = newDocument.get().textDocument();
TextDocument textDocument = newDocument.textDocument();
int textPosition = textDocument.textPositionFrom(request.position());

TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), request.text());
TextDocument newTextDocument =
textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0])));
Document newDoc = newDocument.get().modify()
Document newDoc = newDocument.modify()
.withContent(String.join(System.lineSeparator(), newTextDocument.textLines()))
.apply();

Expand All @@ -210,7 +191,7 @@ public CompletableFuture<FlowModelGeneratorResponse> getSuggestedFlowModel(

ModelGenerator suggestedModelGenerator =
new ModelGenerator(newDoc.module().getCompilation().getSemanticModel(), newDoc,
endLineRange, destination, newDataMappingsDoc.orElse(null));
endLineRange, filePath, newDataMappingsDoc.orElse(null));
JsonElement newFlowModel = suggestedModelGenerator.getFlowModel();

LinePosition endPosition = newTextDocument.linePositionFrom(textPosition + request.text().length());
Expand All @@ -224,24 +205,6 @@ public CompletableFuture<FlowModelGeneratorResponse> getSuggestedFlowModel(
newFlowModel.getAsJsonObject().add("nodes", new JsonArray());
}
response.setFlowDesignModel(newFlowModel);

try {
if (Files.isDirectory(destinationDir)) {
try (Stream<Path> paths = Files.walk(destinationDir)) {
paths.sorted(Comparator.reverseOrder()).forEach(source -> {
try {
Files.delete(source);
} catch (IOException e) {
throw new RuntimeException("Failed to delete destination directory", e);
}
});
}
} else {
Files.delete(destinationDir);
}
} catch (IOException e) {
throw new RuntimeException("Failed to delete destination", e);
}
} catch (Throwable e) {
response.setError(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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 io.ballerina.tools.text.TextDocument;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.stream.Stream;

/**
* Manages the cache of the temporarily copied project directory.
*
* @since 1.4.0
*/
public class ProjectCacheManager {

private final Path sourceDir;
private final Path filePath;
private Path destinationPath;

public ProjectCacheManager(Path sourceDir, Path filePath) {
this.sourceDir = sourceDir;
this.filePath = filePath;
}

public void createTempDirectory() throws IOException {
// Create a temporary directory
Path tempDir = Files.createTempDirectory("project-cache");
Path tempDesintaitonPath = tempDir.resolve(sourceDir.getFileName());
destinationPath = tempDesintaitonPath;

// Copy contents from sourceDir to destinationDir
if (Files.isDirectory(sourceDir)) {
try (Stream<Path> paths = Files.walk(sourceDir)) {
paths.forEach(source -> {
try {
Files.copy(source, tempDesintaitonPath.resolve(sourceDir.relativize(source)),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to copy project directory to cache", e);
}
});
}
return;
}
Files.copy(sourceDir, tempDesintaitonPath, StandardCopyOption.REPLACE_EXISTING);
}

public void deleteCache() throws IOException {
if (Files.isDirectory(destinationPath)) {
try (Stream<Path> paths = Files.walk(destinationPath)) {
paths.sorted(Comparator.reverseOrder()).forEach(source -> {
try {
Files.delete(source);
} catch (IOException e) {
throw new RuntimeException("Failed to delete destination directory", e);
}
});
}
return;
}
Files.delete(destinationPath);
}

public void writeContent(TextDocument textDocument) throws IOException {
if (destinationPath == null) {
throw new RuntimeException("Destination directory is not created");
}
Files.writeString(destinationPath, new String(textDocument.toCharArray()), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}

public Path getDestination() {
if (destinationPath == null) {
throw new RuntimeException("Destination directory is not created");
}
return destinationPath.resolve(sourceDir.relativize(sourceDir.resolve(filePath)));
}
}
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.request;

import com.google.gson.JsonObject;
import io.ballerina.tools.text.LinePosition;
import org.eclipse.lsp4j.CompletionContext;

/**
* Represents a request for expression editor completion.
*
* @param filePath The file path which contains the expression
* @param expression The modified expression
* @param branch The branch of the expression if exists
* @param property The property of the expression
* @param startLine The start line of the node
* @param offset The offset of cursor compared to the start of the expression
* @param context The completion context
* @param node The node which contains the expression
*/
public record ExpressionEditorCompletionRequest(String filePath, String expression, String branch, String property,
LinePosition startLine, int offset, CompletionContext context,
JsonObject node) {
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
io.ballerina.flowmodelgenerator.extension.FlowModelGeneratorService
io.ballerina.flowmodelgenerator.extension.ExpressionEditorService
Loading
Loading