From 8fc1a9bb96b0871a4d6a53f44d62a0c66bd324eb Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Wed, 11 Sep 2024 15:33:27 +0530 Subject: [PATCH 1/6] Add initial implementation of the completion API --- .../extension/ExpressionEditorService.java | 114 ++++ .../extension/ProjectCacheManager.java | 81 +++ .../ExpressionEditorCompletionRequest.java | 41 ++ ....service.spi.ExtendedLanguageServerService | 1 + .../extension/AbstractLSTest.java | 8 +- .../ExpressionEditorCompletionTest.java | 90 +++ .../resources/completions/config/config.json | 637 ++++++++++++++++++ .../resources/completions/source/source.bal | 20 + .../src/test/resources/testng.xml | 1 + 9 files changed, 991 insertions(+), 2 deletions(-) create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/ExpressionEditorCompletionRequest.java create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/config/config.json create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/source/source.bal diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java new file mode 100644 index 000000000..c534f663d --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java @@ -0,0 +1,114 @@ +/* + * 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.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +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, CompletionList>> completion( + ExpressionEditorCompletionRequest request) { + return CompletableFuture.supplyAsync(() -> { + try { + Path filePath = Path.of(request.filePath()); + this.workspaceManager.loadProject(filePath); + Path projectPath = this.workspaceManager.projectRoot(filePath); + + ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath); + projectCacheManager.createTempDirectoryWithContents(); + Path destination = projectCacheManager.getDestination(); + this.workspaceManager.loadProject(destination); + + Optional 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]))); + Files.write(destination, new String(newTextDocument.toCharArray()).getBytes(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + document.get().modify() + .withContent(String.join(System.lineSeparator(), newTextDocument.textLines())) + .apply(); + + 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()); + CompletableFuture, CompletionList>> completableFuture = + langServer.getTextDocumentService().completion(params); + Either, CompletionList> completions = completableFuture.join(); + projectCacheManager.deleteCache(); + return completions; + } catch (Exception ignored) { + return Either.forLeft(List.of()); + } + }); + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java new file mode 100644 index 000000000..9752e3f39 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java @@ -0,0 +1,81 @@ +/* + * 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 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.stream.Stream; + +public class ProjectCacheManager { + + private final Path sourceDir; + private final Path filePath; + private Path destinationDir; + + public ProjectCacheManager(Path sourceDir, Path filePath) { + this.sourceDir = sourceDir; + this.filePath = filePath; + } + + public void createTempDirectoryWithContents() throws IOException { + // Create a temporary directory + Path tempDir = Files.createTempDirectory("project-cache"); + destinationDir = tempDir.resolve(sourceDir.getFileName()); + + // Copy contents from sourceDir to destinationDir + if (Files.isDirectory(sourceDir)) { + try (Stream paths = Files.walk(sourceDir)) { + paths.forEach(source -> { + try { + Files.copy(source, destinationDir.resolve(sourceDir.relativize(source)), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException("Failed to copy project directory to cache", e); + } + }); + } + } else { + Files.copy(sourceDir, destinationDir, StandardCopyOption.REPLACE_EXISTING); + } + } + + public void deleteCache() throws IOException { + if (Files.isDirectory(destinationDir)) { + try (Stream 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); + } + } + + public Path getDestination() { + return destinationDir.resolve(sourceDir.relativize(sourceDir.resolve(filePath))); + } +} \ No newline at end of file diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/ExpressionEditorCompletionRequest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/ExpressionEditorCompletionRequest.java new file mode 100644 index 000000000..83835b9fd --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/request/ExpressionEditorCompletionRequest.java @@ -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) { +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService index 6885025d4..4ffecb4a2 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/resources/META-INF/services/org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService @@ -1 +1,2 @@ io.ballerina.flowmodelgenerator.extension.FlowModelGeneratorService +io.ballerina.flowmodelgenerator.extension.ExpressionEditorService diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/AbstractLSTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/AbstractLSTest.java index ad8e4ead0..d1dc52239 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/AbstractLSTest.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/AbstractLSTest.java @@ -112,7 +112,7 @@ protected Object[] getConfigsList() { * @return The list of tests to be skipped */ protected String[] skipList() { - return new String[]{}; + return new String[]{ }; } /** @@ -137,7 +137,7 @@ protected JsonObject getResponse(Endpoint endpoint, Object request) { } protected JsonObject getResponse(Endpoint endpoint, Object request, String api) { - CompletableFuture result = endpoint.request("flowDesignService/" + api, request); + CompletableFuture result = endpoint.request(getServiceName() + "/" + api, request); String response = TestUtil.getResponseString(result); JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject().getAsJsonObject("result"); JsonPrimitive errorMsg = jsonObject.getAsJsonPrimitive("errorMsg"); @@ -278,6 +278,10 @@ private void compareJsonArrays(JsonArray actualArray, JsonArray expectedArray, S */ protected abstract String getApiName(); + protected String getServiceName() { + return "flowDesignService"; + } + @AfterClass public void shutDownLanguageServer() { TestUtil.shutdownLanguageServer(this.serviceEndpoint); diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java new file mode 100644 index 000000000..ef8f3aa80 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java @@ -0,0 +1,90 @@ +/* + * 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.ExpressionEditorCompletionRequest; +import io.ballerina.tools.text.LinePosition; +import org.eclipse.lsp4j.CompletionContext; +import org.eclipse.lsp4j.CompletionItem; +import org.testng.Assert; +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 completion service. + * + * @since 1.4.0 + */ +public class ExpressionEditorCompletionTest 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); + + ExpressionEditorCompletionRequest request = new ExpressionEditorCompletionRequest( + sourceDir.resolve(testConfig.filePath()).toAbsolutePath().toString(), testConfig.expression(), + testConfig.branch(), testConfig.property(), testConfig.startLine(), testConfig.offset(), + testConfig.context(), testConfig.node()); + JsonObject response = getResponse(request); + + List actualCompletions = gson.fromJson(response.get("left").getAsJsonArray(), + new TypeToken>() { }.getType()); + if (!assertArray("completions", actualCompletions, testConfig.completions())) { + TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.filePath(), + testConfig.expression(), testConfig.branch(), testConfig.property(), testConfig.startLine(), + testConfig.offset(), testConfig.context(), testConfig.node(), actualCompletions); + updateConfig(configJsonPath, updatedConfig); + Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath)); + } + } + + @Override + protected String getResourceDir() { + return "completions"; + } + + @Override + protected Class clazz() { + return ExpressionEditorCompletionTest.class; + } + + @Override + protected String getApiName() { + return "completion"; + } + + @Override + protected String getServiceName() { + return "expressionEditor"; + } + + private record TestConfig(String description, String filePath, String expression, String branch, String property, + LinePosition startLine, int offset, CompletionContext context, JsonObject node, + List completions) { + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/config/config.json b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/config/config.json new file mode 100644 index 000000000..c622d0122 --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/config/config.json @@ -0,0 +1,637 @@ +{ + "description": "", + "filePath": "source.bal", + "expression": "self.classVar > local + 21", + "branch": "self.classVar > l", + "property": "condition", + "startLine": { + "line": 13, + "offset": 8 + }, + "offset": 22, + "context": { + "triggerKind": "Invoked" + }, + "node": { + "id": "45021", + "metadata": { + "label": "If", + "description": "Add conditional branch to the integration flow." + }, + "codedata": { + "node": "IF", + "lineRange": { + "fileName": "test.bal", + "startLine": { + "line": 13, + "offset": 8 + }, + "endLine": { + "line": 17, + "offset": 9 + } + }, + "sourceCode": "if (moduleVar > 11) {\n int i = 32;\n } else if self.classVar > l {\n\n }" + }, + "returning": false, + "branches": [ + { + "label": "Then", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "test.bal", + "startLine": { + "line": 13, + "offset": 28 + }, + "endLine": { + "line": 15, + "offset": 9 + } + }, + "sourceCode": "{\n int i = 32;\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "valueType": "EXPRESSION", + "value": "(moduleVar > 11) ", + "optional": false, + "editable": true + } + }, + "children": [ + { + "id": "46027", + "metadata": { + "label": "New Variable", + "description": "New variable 'i' with type 'int'" + }, + "codedata": { + "node": "NEW_DATA", + "lineRange": { + "fileName": "test.bal", + "startLine": { + "line": 14, + "offset": 12 + }, + "endLine": { + "line": 14, + "offset": 23 + } + }, + "sourceCode": "int i = 32;" + }, + "returning": false, + "properties": { + "expression": { + "metadata": { + "label": "Expression", + "description": "Expression" + }, + "valueType": "EXPRESSION", + "value": "32", + "optional": false, + "editable": true + }, + "variable": { + "metadata": { + "label": "Data variable", + "description": "Name of the variable" + }, + "valueType": "IDENTIFIER", + "value": "i", + "optional": false, + "editable": true + }, + "type": { + "metadata": { + "label": "Data type", + "description": "Type of the variable" + }, + "valueType": "TYPE", + "value": "int", + "optional": false, + "editable": true + } + }, + "flags": 0 + } + ] + }, + { + "label": "self.classVar > l", + "kind": "BLOCK", + "codedata": { + "node": "CONDITIONAL", + "lineRange": { + "fileName": "test.bal", + "startLine": { + "line": 15, + "offset": 36 + }, + "endLine": { + "line": 17, + "offset": 9 + } + }, + "sourceCode": "{\n\n }" + }, + "repeatable": "ONE_OR_MORE", + "properties": { + "condition": { + "metadata": { + "label": "Condition", + "description": "Boolean Condition" + }, + "valueType": "EXPRESSION", + "value": "self.classVar > l ", + "optional": false, + "editable": true + } + }, + "children": [] + } + ], + "flags": 0 + }, + "completions": [ + { + "label": "start", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "start", + "insertText": "start ", + "insertTextFormat": "Snippet" + }, + { + "label": "wait", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "wait", + "insertText": "wait ", + "insertTextFormat": "Snippet" + }, + { + "label": "flush", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "flush", + "insertText": "flush ", + "insertTextFormat": "Snippet" + }, + { + "label": "from clause", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "from", + "insertText": "from ${1:var} ${2:item} in ${3}", + "insertTextFormat": "Snippet" + }, + { + "label": "http", + "kind": "Module", + "detail": "Module", + "sortText": "S", + "filterText": "http", + "insertText": "http", + "insertTextFormat": "Snippet" + }, + { + "label": "decimal", + "kind": "TypeParameter", + "detail": "Decimal", + "sortText": "R", + "insertText": "decimal", + "insertTextFormat": "Snippet" + }, + { + "label": "error", + "kind": "Event", + "detail": "Error", + "sortText": "P", + "insertText": "error", + "insertTextFormat": "Snippet" + }, + { + "label": "object", + "kind": "Unit", + "detail": "type", + "sortText": "R", + "insertText": "object", + "insertTextFormat": "Snippet" + }, + { + "label": "transaction", + "kind": "Unit", + "detail": "type", + "sortText": "R", + "insertText": "transaction", + "insertTextFormat": "Snippet" + }, + { + "label": "xml", + "kind": "TypeParameter", + "detail": "Xml", + "sortText": "R", + "insertText": "xml", + "insertTextFormat": "Snippet" + }, + { + "label": "table", + "kind": "Unit", + "detail": "type", + "sortText": "R", + "insertText": "table", + "insertTextFormat": "Snippet" + }, + { + "label": "map", + "kind": "Unit", + "detail": "type", + "sortText": "R", + "insertText": "map", + "insertTextFormat": "Snippet" + }, + { + "label": "stream", + "kind": "Unit", + "detail": "type", + "sortText": "R", + "insertText": "stream", + "insertTextFormat": "Snippet" + }, + { + "label": "boolean", + "kind": "TypeParameter", + "detail": "Boolean", + "sortText": "R", + "insertText": "boolean", + "insertTextFormat": "Snippet" + }, + { + "label": "future", + "kind": "TypeParameter", + "detail": "Future", + "sortText": "R", + "insertText": "future", + "insertTextFormat": "Snippet" + }, + { + "label": "int", + "kind": "TypeParameter", + "detail": "Int", + "sortText": "R", + "insertText": "int", + "insertTextFormat": "Snippet" + }, + { + "label": "float", + "kind": "TypeParameter", + "detail": "Float", + "sortText": "R", + "insertText": "float", + "insertTextFormat": "Snippet" + }, + { + "label": "function", + "kind": "TypeParameter", + "detail": "Function", + "sortText": "R", + "insertText": "function", + "insertTextFormat": "Snippet" + }, + { + "label": "string", + "kind": "TypeParameter", + "detail": "String", + "sortText": "R", + "insertText": "string", + "insertTextFormat": "Snippet" + }, + { + "label": "typedesc", + "kind": "TypeParameter", + "detail": "Typedesc", + "sortText": "R", + "insertText": "typedesc", + "insertTextFormat": "Snippet" + }, + { + "label": "service", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "service", + "insertText": "service", + "insertTextFormat": "Snippet" + }, + { + "label": "new", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "new", + "insertText": "new ", + "insertTextFormat": "Snippet" + }, + { + "label": "isolated", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "isolated", + "insertText": "isolated ", + "insertTextFormat": "Snippet" + }, + { + "label": "transactional", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "transactional", + "insertText": "transactional", + "insertTextFormat": "Snippet" + }, + { + "label": "function", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "function", + "insertText": "function ", + "insertTextFormat": "Snippet" + }, + { + "label": "let", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "let", + "insertText": "let", + "insertTextFormat": "Snippet" + }, + { + "label": "typeof", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "typeof", + "insertText": "typeof ", + "insertTextFormat": "Snippet" + }, + { + "label": "trap", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "trap", + "insertText": "trap", + "insertTextFormat": "Snippet" + }, + { + "label": "client", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "client", + "insertText": "client ", + "insertTextFormat": "Snippet" + }, + { + "label": "true", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "true", + "insertText": "true", + "insertTextFormat": "Snippet" + }, + { + "label": "false", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "false", + "insertText": "false", + "insertTextFormat": "Snippet" + }, + { + "label": "null", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "null", + "insertText": "null", + "insertTextFormat": "Snippet" + }, + { + "label": "check", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "check", + "insertText": "check ", + "insertTextFormat": "Snippet" + }, + { + "label": "checkpanic", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "checkpanic", + "insertText": "checkpanic ", + "insertTextFormat": "Snippet" + }, + { + "label": "is", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "is", + "insertText": "is", + "insertTextFormat": "Snippet" + }, + { + "label": "error constructor", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "error", + "insertText": "error(\"${1}\")", + "insertTextFormat": "Snippet" + }, + { + "label": "object constructor", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "object", + "insertText": "object {${1}}", + "insertTextFormat": "Snippet" + }, + { + "label": "base16", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "base16", + "insertText": "base16 `${1}`", + "insertTextFormat": "Snippet" + }, + { + "label": "base64", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "base64", + "insertText": "base64 `${1}`", + "insertTextFormat": "Snippet" + }, + { + "label": "from", + "kind": "Keyword", + "detail": "Keyword", + "sortText": "U", + "filterText": "from", + "insertText": "from ", + "insertTextFormat": "Snippet" + }, + { + "label": "re ``", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "re ``", + "insertText": "re `${1}`", + "insertTextFormat": "Snippet" + }, + { + "label": "string ``", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "string ``", + "insertText": "string `${1}`", + "insertTextFormat": "Snippet" + }, + { + "label": "xml ``", + "kind": "Snippet", + "detail": "Snippet", + "sortText": "T", + "filterText": "xml ``", + "insertText": "xml `${1}`", + "insertTextFormat": "Snippet" + }, + { + "label": "StrandData", + "kind": "Struct", + "detail": "Record", + "documentation": { + "left": "Describes Strand execution details for the runtime.\n" + }, + "sortText": "Q", + "insertText": "StrandData", + "insertTextFormat": "Snippet" + }, + { + "label": "Thread", + "kind": "TypeParameter", + "detail": "Union", + "sortText": "R", + "insertText": "Thread", + "insertTextFormat": "Snippet" + }, + { + "label": "localVar", + "kind": "Variable", + "detail": "float", + "sortText": "F", + "insertText": "localVar", + "insertTextFormat": "Snippet" + }, + { + "label": "moduleVar", + "kind": "Variable", + "detail": "int", + "sortText": "AB", + "insertText": "moduleVar", + "insertTextFormat": "Snippet" + }, + { + "label": "self", + "kind": "Variable", + "detail": "service object {int classVar; resource function get test () returns ();}", + "sortText": "F", + "insertText": "self", + "insertTextFormat": "Snippet" + }, + { + "label": "readonly", + "kind": "TypeParameter", + "detail": "Readonly", + "sortText": "R", + "insertText": "readonly", + "insertTextFormat": "Snippet" + }, + { + "label": "handle", + "kind": "TypeParameter", + "detail": "Handle", + "sortText": "R", + "insertText": "handle", + "insertTextFormat": "Snippet" + }, + { + "label": "never", + "kind": "TypeParameter", + "detail": "Never", + "sortText": "R", + "insertText": "never", + "insertTextFormat": "Snippet" + }, + { + "label": "json", + "kind": "TypeParameter", + "detail": "Json", + "sortText": "R", + "insertText": "json", + "insertTextFormat": "Snippet" + }, + { + "label": "anydata", + "kind": "TypeParameter", + "detail": "Anydata", + "sortText": "R", + "insertText": "anydata", + "insertTextFormat": "Snippet" + }, + { + "label": "any", + "kind": "TypeParameter", + "detail": "Any", + "sortText": "R", + "insertText": "any", + "insertTextFormat": "Snippet" + }, + { + "label": "byte", + "kind": "TypeParameter", + "detail": "Byte", + "sortText": "R", + "insertText": "byte", + "insertTextFormat": "Snippet" + } + ] +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/source/source.bal b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/source/source.bal new file mode 100644 index 000000000..68ebd417d --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/completions/source/source.bal @@ -0,0 +1,20 @@ +import ballerina/http; + +// Module-level variable +int moduleVar = 10; + +service / on new http:Listener(8080) { + // Class-level variable + private int classVar = 20; + + resource function get test() { + // Local-level variable + float localVar = 3.14; + + if (moduleVar > 11) { + int i = 32; + } else if (self.classVar > ) { + + } + } +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml index 2f0b62463..c8f18f629 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/resources/testng.xml @@ -31,6 +31,7 @@ under the License. + From d24cf8ac37942de45100d7291391f6f8a9c4b45f Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Fri, 13 Sep 2024 11:33:14 +0530 Subject: [PATCH 2/6] Introduce a cache manager for temp directories --- .../extension/ExpressionEditorService.java | 5 +-- .../extension/FlowModelGeneratorService.java | 44 +++---------------- .../extension/ProjectCacheManager.java | 2 +- 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java index c534f663d..4abad515c 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java @@ -73,7 +73,7 @@ public CompletableFuture, CompletionList>> completio Path projectPath = this.workspaceManager.projectRoot(filePath); ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath); - projectCacheManager.createTempDirectoryWithContents(); + projectCacheManager.createTempDirectory(); Path destination = projectCacheManager.getDestination(); this.workspaceManager.loadProject(destination); @@ -97,8 +97,7 @@ public CompletableFuture, CompletionList>> completio Position position = new Position(request.startLine().line(), request.startLine().offset() + 4 + request.offset()); - TextDocumentIdentifier identifier = - new TextDocumentIdentifier(destination.toUri().toString()); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(destination.toUri().toString()); CompletionParams params = new CompletionParams(identifier, position, request.context()); CompletableFuture, CompletionList>> completableFuture = diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java index cd5d82415..d8fe48018 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java @@ -62,15 +62,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. @@ -162,25 +158,11 @@ public CompletableFuture getSuggestedFlowModel( Path tempDir = Files.createTempDirectory("project-cache"); Path destinationDir = tempDir.resolve(projectPath.getFileName()); - if (Files.isDirectory(projectPath)) { - try (Stream 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); - } + ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath); + projectCacheManager.createTempDirectory(); - Path destination = destinationDir.resolve(projectPath.relativize(projectPath.resolve(filePath))); - Project newProject = this.workspaceManager.loadProject(destination); + Path destination = projectCacheManager.getDestination(); + this.workspaceManager.loadProject(destination); Optional newSemanticModel = this.workspaceManager.semanticModel(destination); Optional newDocument = this.workspaceManager.document(destination); if (newSemanticModel.isEmpty() || newDocument.isEmpty()) { @@ -225,23 +207,7 @@ public CompletableFuture getSuggestedFlowModel( } response.setFlowDesignModel(newFlowModel); - try { - if (Files.isDirectory(destinationDir)) { - try (Stream 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); - } + projectCacheManager.deleteCache(); } catch (Throwable e) { response.setError(e); } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java index 9752e3f39..e139fa48f 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java @@ -37,7 +37,7 @@ public ProjectCacheManager(Path sourceDir, Path filePath) { this.filePath = filePath; } - public void createTempDirectoryWithContents() throws IOException { + public void createTempDirectory() throws IOException { // Create a temporary directory Path tempDir = Files.createTempDirectory("project-cache"); destinationDir = tempDir.resolve(sourceDir.getFileName()); From 20511acc91896f50720a02bd218b3ef31385a41e Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Fri, 13 Sep 2024 11:56:36 +0530 Subject: [PATCH 3/6] Remove cache manager from editor completions --- .../extension/ExpressionEditorService.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java index 4abad515c..8936e9067 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java @@ -21,6 +21,9 @@ import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorCompletionRequest; 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.TextDocument; import io.ballerina.tools.text.TextDocumentChange; import io.ballerina.tools.text.TextEdit; @@ -42,7 +45,6 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; @JavaSPIService("org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService") @@ -69,19 +71,16 @@ public CompletableFuture, CompletionList>> completio return CompletableFuture.supplyAsync(() -> { try { Path filePath = Path.of(request.filePath()); - this.workspaceManager.loadProject(filePath); - Path projectPath = this.workspaceManager.projectRoot(filePath); + Project project = this.workspaceManager.loadProject(filePath); - ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath); - projectCacheManager.createTempDirectory(); - Path destination = projectCacheManager.getDestination(); - this.workspaceManager.loadProject(destination); - - Optional document = this.workspaceManager.document(destination); - if (document.isEmpty()) { + DocumentId documentId = project.documentId(filePath); + Module module = project.currentPackage().module(documentId.moduleId()); + Document document = module.document(documentId); + if (document == null) { return Either.forLeft(List.of()); } - TextDocument textDocument = document.get().textDocument(); + + TextDocument textDocument = document.textDocument(); // Determine the cursor position int textPosition = textDocument.textPositionFrom(request.startLine()); @@ -89,21 +88,18 @@ public CompletableFuture, CompletionList>> completio TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), statement); TextDocument newTextDocument = textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0]))); - Files.write(destination, new String(newTextDocument.toCharArray()).getBytes(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - document.get().modify() + document.modify() .withContent(String.join(System.lineSeparator(), newTextDocument.textLines())) .apply(); Position position = new Position(request.startLine().line(), request.startLine().offset() + 4 + request.offset()); - TextDocumentIdentifier identifier = new TextDocumentIdentifier(destination.toUri().toString()); + TextDocumentIdentifier identifier = new TextDocumentIdentifier(filePath.toUri().toString()); CompletionParams params = new CompletionParams(identifier, position, request.context()); CompletableFuture, CompletionList>> completableFuture = langServer.getTextDocumentService().completion(params); Either, CompletionList> completions = completableFuture.join(); - projectCacheManager.deleteCache(); return completions; } catch (Exception ignored) { return Either.forLeft(List.of()); From a4122a502153edfdd862779624c4845614937ec2 Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Fri, 13 Sep 2024 14:58:03 +0530 Subject: [PATCH 4/6] Remove project cache manager --- .../extension/FlowModelGeneratorService.java | 35 ++++---- .../extension/ProjectCacheManager.java | 81 ------------------- 2 files changed, 16 insertions(+), 100 deletions(-) delete mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java index d8fe48018..96148cf1b 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/FlowModelGeneratorService.java @@ -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; @@ -134,7 +136,7 @@ public CompletableFuture 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 = this.workspaceManager.semanticModel(filePath); Optional document = this.workspaceManager.document(filePath); if (semanticModel.isEmpty() || document.isEmpty()) { @@ -155,34 +157,31 @@ public CompletableFuture 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()); - - ProjectCacheManager projectCacheManager = new ProjectCacheManager(projectPath, filePath); - projectCacheManager.createTempDirectory(); - - Path destination = projectCacheManager.getDestination(); - this.workspaceManager.loadProject(destination); - Optional newSemanticModel = this.workspaceManager.semanticModel(destination); - Optional 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 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(); @@ -192,7 +191,7 @@ public CompletableFuture 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()); @@ -206,8 +205,6 @@ public CompletableFuture getSuggestedFlowModel( newFlowModel.getAsJsonObject().add("nodes", new JsonArray()); } response.setFlowDesignModel(newFlowModel); - - projectCacheManager.deleteCache(); } catch (Throwable e) { response.setError(e); } diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java deleted file mode 100644 index e139fa48f..000000000 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 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.stream.Stream; - -public class ProjectCacheManager { - - private final Path sourceDir; - private final Path filePath; - private Path destinationDir; - - 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"); - destinationDir = tempDir.resolve(sourceDir.getFileName()); - - // Copy contents from sourceDir to destinationDir - if (Files.isDirectory(sourceDir)) { - try (Stream paths = Files.walk(sourceDir)) { - paths.forEach(source -> { - try { - Files.copy(source, destinationDir.resolve(sourceDir.relativize(source)), - StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - throw new RuntimeException("Failed to copy project directory to cache", e); - } - }); - } - } else { - Files.copy(sourceDir, destinationDir, StandardCopyOption.REPLACE_EXISTING); - } - } - - public void deleteCache() throws IOException { - if (Files.isDirectory(destinationDir)) { - try (Stream 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); - } - } - - public Path getDestination() { - return destinationDir.resolve(sourceDir.relativize(sourceDir.resolve(filePath))); - } -} \ No newline at end of file From d465aa4068ff37c07ede9ca399fd1424e32c1336 Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Sat, 14 Sep 2024 23:02:11 +0530 Subject: [PATCH 5/6] Manage expression editor completions via cache --- .../extension/ExpressionEditorService.java | 35 +++++--- .../extension/ProjectCacheManager.java | 85 +++++++++++++++++++ 2 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java index 8936e9067..5668177c6 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java @@ -21,9 +21,6 @@ import io.ballerina.flowmodelgenerator.extension.request.ExpressionEditorCompletionRequest; 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.TextDocument; import io.ballerina.tools.text.TextDocumentChange; import io.ballerina.tools.text.TextEdit; @@ -45,6 +42,7 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; @JavaSPIService("org.ballerinalang.langserver.commons.service.spi.ExtendedLanguageServerService") @@ -70,17 +68,23 @@ public CompletableFuture, CompletionList>> completio ExpressionEditorCompletionRequest request) { return CompletableFuture.supplyAsync(() -> { try { + // Load the project Path filePath = Path.of(request.filePath()); - Project project = this.workspaceManager.loadProject(filePath); + this.workspaceManager.loadProject(filePath); + Path projectPath = this.workspaceManager.projectRoot(filePath); - DocumentId documentId = project.documentId(filePath); - Module module = project.currentPackage().module(documentId.moduleId()); - Document document = module.document(documentId); - if (document == null) { + // 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 = this.workspaceManager.document(destination); + if (document.isEmpty()) { return Either.forLeft(List.of()); } - - TextDocument textDocument = document.textDocument(); + TextDocument textDocument = document.get().textDocument(); // Determine the cursor position int textPosition = textDocument.textPositionFrom(request.startLine()); @@ -88,18 +92,23 @@ public CompletableFuture, CompletionList>> completio TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), statement); TextDocument newTextDocument = textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0]))); - document.modify() + Files.write(destination, new String(newTextDocument.toCharArray()).getBytes(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + 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(filePath.toUri().toString()); - + TextDocumentIdentifier identifier = new TextDocumentIdentifier(destination.toUri().toString()); CompletionParams params = new CompletionParams(identifier, position, request.context()); + + // Get the completions CompletableFuture, CompletionList>> completableFuture = langServer.getTextDocumentService().completion(params); Either, CompletionList> completions = completableFuture.join(); + projectCacheManager.deleteCache(); return completions; } catch (Exception ignored) { return Either.forLeft(List.of()); diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java new file mode 100644 index 000000000..ff94fa15e --- /dev/null +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java @@ -0,0 +1,85 @@ +/* + * 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 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.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 destinationDir; + + 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"); + destinationDir = tempDir.resolve(sourceDir.getFileName()); + + // Copy contents from sourceDir to destinationDir + if (Files.isDirectory(sourceDir)) { + try (Stream paths = Files.walk(sourceDir)) { + paths.forEach(source -> { + try { + Files.copy(source, destinationDir.resolve(sourceDir.relativize(source)), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException("Failed to copy project directory to cache", e); + } + }); + } + } else { + Files.copy(sourceDir, destinationDir, StandardCopyOption.REPLACE_EXISTING); + } + } + + public void deleteCache() throws IOException { + if (Files.isDirectory(destinationDir)) { + try (Stream 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); + } + } + + public Path getDestination() { + return destinationDir.resolve(sourceDir.relativize(sourceDir.resolve(filePath))); + } +} \ No newline at end of file From fc7db6641d711fce78c922bf917c5d6c2e21fa62 Mon Sep 17 00:00:00 2001 From: Nipuna Fernando Date: Sat, 14 Sep 2024 23:12:54 +0530 Subject: [PATCH 6/6] Fix spotbug failure --- .../extension/ExpressionEditorService.java | 7 +--- .../extension/ProjectCacheManager.java | 37 +++++++++++++------ .../ExpressionEditorCompletionTest.java | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java index 5668177c6..af85b55ea 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorService.java @@ -38,9 +38,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonSegment; import org.eclipse.lsp4j.services.LanguageServer; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -92,8 +90,7 @@ public CompletableFuture, CompletionList>> completio TextEdit textEdit = TextEdit.from(TextRange.from(textPosition, 0), statement); TextDocument newTextDocument = textDocument.apply(TextDocumentChange.from(List.of(textEdit).toArray(new TextEdit[0]))); - Files.write(destination, new String(newTextDocument.toCharArray()).getBytes(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + projectCacheManager.writeContent(newTextDocument); document.get().modify() .withContent(String.join(System.lineSeparator(), newTextDocument.textLines())) .apply(); @@ -110,7 +107,7 @@ public CompletableFuture, CompletionList>> completio Either, CompletionList> completions = completableFuture.join(); projectCacheManager.deleteCache(); return completions; - } catch (Exception ignored) { + } catch (Throwable e) { return Either.forLeft(List.of()); } }); diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java index ff94fa15e..469e101e7 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/main/java/io/ballerina/flowmodelgenerator/extension/ProjectCacheManager.java @@ -18,10 +18,13 @@ 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; @@ -34,7 +37,7 @@ public class ProjectCacheManager { private final Path sourceDir; private final Path filePath; - private Path destinationDir; + private Path destinationPath; public ProjectCacheManager(Path sourceDir, Path filePath) { this.sourceDir = sourceDir; @@ -44,28 +47,29 @@ public ProjectCacheManager(Path sourceDir, Path filePath) { public void createTempDirectory() throws IOException { // Create a temporary directory Path tempDir = Files.createTempDirectory("project-cache"); - destinationDir = tempDir.resolve(sourceDir.getFileName()); + Path tempDesintaitonPath = tempDir.resolve(sourceDir.getFileName()); + destinationPath = tempDesintaitonPath; // Copy contents from sourceDir to destinationDir if (Files.isDirectory(sourceDir)) { try (Stream paths = Files.walk(sourceDir)) { paths.forEach(source -> { try { - Files.copy(source, destinationDir.resolve(sourceDir.relativize(source)), + 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); } }); } - } else { - Files.copy(sourceDir, destinationDir, StandardCopyOption.REPLACE_EXISTING); + return; } + Files.copy(sourceDir, tempDesintaitonPath, StandardCopyOption.REPLACE_EXISTING); } public void deleteCache() throws IOException { - if (Files.isDirectory(destinationDir)) { - try (Stream paths = Files.walk(destinationDir)) { + if (Files.isDirectory(destinationPath)) { + try (Stream paths = Files.walk(destinationPath)) { paths.sorted(Comparator.reverseOrder()).forEach(source -> { try { Files.delete(source); @@ -74,12 +78,23 @@ public void deleteCache() throws IOException { } }); } - } else { - Files.delete(destinationDir); + 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() { - return destinationDir.resolve(sourceDir.relativize(sourceDir.resolve(filePath))); + if (destinationPath == null) { + throw new RuntimeException("Destination directory is not created"); + } + return destinationPath.resolve(sourceDir.relativize(sourceDir.resolve(filePath))); } -} \ No newline at end of file +} diff --git a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java index ef8f3aa80..6289655a7 100644 --- a/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java +++ b/flow-model-generator/modules/flow-model-generator-ls-extension/src/test/java/io/ballerina/flowmodelgenerator/extension/ExpressionEditorCompletionTest.java @@ -58,7 +58,7 @@ public void test(Path config) throws IOException { TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.filePath(), testConfig.expression(), testConfig.branch(), testConfig.property(), testConfig.startLine(), testConfig.offset(), testConfig.context(), testConfig.node(), actualCompletions); - updateConfig(configJsonPath, updatedConfig); +// updateConfig(configJsonPath, updatedConfig); Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath)); } }