Skip to content

Commit

Permalink
Merge pull request #403 from nipunayf/completions-api
Browse files Browse the repository at this point in the history
Provide completions for expressions in the expression editor
  • Loading branch information
nipunayf authored Sep 26, 2024
2 parents c96deaa + fc7db66 commit 1e628b7
Show file tree
Hide file tree
Showing 10 changed files with 1,027 additions and 55 deletions.
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

0 comments on commit 1e628b7

Please sign in to comment.