diff --git a/.github/workflows/language_server_simulator_fhir.yml b/.github/workflows/language_server_simulator_fhir.yml new file mode 100644 index 0000000000..fa9a4e5ac9 --- /dev/null +++ b/.github/workflows/language_server_simulator_fhir.yml @@ -0,0 +1,81 @@ +name: Language Server Simulator on FHIR + +on: + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +jobs: + run_simulator: + name: Run LS Simulator + runs-on: ubuntu-latest + timeout-minutes: 240 + strategy: + fail-fast: false + matrix: + branch: [ "master", "2201.8.x", "2201.7.x" ] + skipGenerators: [ "", "IMPORT_STATEMENT" ] + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + with: + ref: ${{ matrix.branch }} + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '11' + + - name: Initialize sub-modules + run: git submodule update --init + + - name: Build with Gradle + timeout-minutes: 180 + env: + packageUser: ${{ secrets.PACKAGE_USER}} + packagePAT: ${{ secrets.PACKAGE_PAT}} + run: | + export DISPLAY=':99.0' + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + ./gradlew clean :language-server-simulator:runLSSimulatorOnFHIR -Dls.simulation.skipGenerators=${{ matrix.skipGenerators }} + + - name: Check Simulation Failure + run: if test -f dump.hprof; then exit 1; else exit 0; fi + + - name: Analyze Heap Dump If Exists + if: failure() + run: | + if test -f dump.hprof; then echo "Heap sump exists. Analyzing..."; else exit 0; fi + wget https://ftp.jaist.ac.jp/pub/eclipse/mat/1.12.0/rcp/MemoryAnalyzer-1.12.0.20210602-linux.gtk.x86_64.zip + unzip MemoryAnalyzer-1.12.0.20210602-linux.gtk.x86_64.zip + ./mat/ParseHeapDump.sh ./dump.hprof org.eclipse.mat.api:suspects + + - name: Upload Heap Dumps + uses: actions/upload-artifact@v2 + if: always() + with: + name: heap_dump-${{ matrix.branch }}.hprof + path: '*.hprof' + + - name: Upload Leaks Suspects + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Leak_Suspects-${{ matrix.branch }} + path: 'dump_Leak_Suspects.zip' + + - name: Notify failure + if: failure() + run: | + curl -X POST \ + 'https://api.github.com/repos/ballerina-platform/ballerina-release/dispatches' \ + -H 'Accept: application/vnd.github.v3+json' \ + -H 'Authorization: Bearer ${{ secrets.BALLERINA_BOT_TOKEN }}' \ + --data "{ + \"event_type\": \"notify-simulator-failure\", + \"client_payload\": { + \"branch\": \"${{ matrix.branch }}\" + } + }" diff --git a/.github/workflows/language_server_simulator_nballerina.yml b/.github/workflows/language_server_simulator_nballerina.yml new file mode 100644 index 0000000000..93d121274f --- /dev/null +++ b/.github/workflows/language_server_simulator_nballerina.yml @@ -0,0 +1,81 @@ +name: Language Server Simulator on nBallerina + +on: + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +jobs: + run_simulator: + name: Run LS Simulator + runs-on: ubuntu-latest + timeout-minutes: 240 + strategy: + fail-fast: false + matrix: + branch: [ "master", "2201.8.x", "2201.7.x" ] + skipGenerators: [ "", "IMPORT_STATEMENT" ] + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + with: + ref: ${{ matrix.branch }} + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '11' + + - name: Initialize sub-modules + run: git submodule update --init + + - name: Build with Gradle + timeout-minutes: 180 + env: + packageUser: ${{ secrets.PACKAGE_USER}} + packagePAT: ${{ secrets.PACKAGE_PAT}} + run: | + export DISPLAY=':99.0' + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + ./gradlew clean :language-server-simulator:runLSSimulatorOnnBallerina -Dls.simulation.skipGenerators=${{ matrix.skipGenerators }} + + - name: Check Simulation Failure + run: if test -f dump.hprof; then exit 1; else exit 0; fi + + - name: Analyze Heap Dump If Exists + if: failure() + run: | + if test -f dump.hprof; then echo "Heap sump exists. Analyzing..."; else exit 0; fi + wget https://ftp.jaist.ac.jp/pub/eclipse/mat/1.12.0/rcp/MemoryAnalyzer-1.12.0.20210602-linux.gtk.x86_64.zip + unzip MemoryAnalyzer-1.12.0.20210602-linux.gtk.x86_64.zip + ./mat/ParseHeapDump.sh ./dump.hprof org.eclipse.mat.api:suspects + + - name: Upload Heap Dumps + uses: actions/upload-artifact@v2 + if: always() + with: + name: heap_dump-${{ matrix.branch }}.hprof + path: '*.hprof' + + - name: Upload Leaks Suspects + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Leak_Suspects-${{ matrix.branch }} + path: 'dump_Leak_Suspects.zip' + + - name: Notify failure + if: failure() + run: | + curl -X POST \ + 'https://api.github.com/repos/ballerina-platform/ballerina-release/dispatches' \ + -H 'Accept: application/vnd.github.v3+json' \ + -H 'Authorization: Bearer ${{ secrets.BALLERINA_BOT_TOKEN }}' \ + --data "{ + \"event_type\": \"notify-simulator-failure\", + \"client_payload\": { + \"branch\": \"${{ matrix.branch }}\" + } + }" diff --git a/language-server-simulator/build.gradle b/language-server-simulator/build.gradle new file mode 100644 index 0000000000..4979967725 --- /dev/null +++ b/language-server-simulator/build.gradle @@ -0,0 +1,138 @@ +description = 'Ballerina Language Server Simulator' + +apply from: "$rootDir/gradle/javaProject.gradle" + +configurations { + ballerinaDistribution + jBallerinaDistribution + dependency { + transitive true + } +} + +ext { + distributionDir = "distribution" + nbalSourceDir = "nBallerinaSrc" + fhirSourceDir = "fhirSrc" + shortVersion = "${version}".split("-")[0] +} + +dependencies { + implementation "org.slf4j:slf4j-api:${project.slf4jApiVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'language-server-core', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'language-server-commons', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-test-utils', version: "${ballerinaLangVersion}" + implementation(group: 'org.eclipse.lsp4j', name:'org.eclipse.lsp4j', version:"${eclipseLsp4jVersion}") { + exclude group: 'com.google.guava', module: 'guava' + } + implementation (group: 'org.eclipse.lsp4j', name:'org.eclipse.lsp4j.jsonrpc', version:"${eclipseLsp4jJsonrpcVersion}"){ + exclude group: 'com.google.guava', module: 'guava' + } + implementation "org.slf4j:slf4j-jdk14:${slf4jJdk14Version}" + implementation "com.google.code.gson:gson:${gsonVersion}" +} + +task unpackBallerinaDistribution(type: Copy) { + def sourceDir = "${buildDir}/${distributionDir}" + from zipTree { "${rootDir}/ballerina/build/distributions/ballerina-${version}-swan-lake.zip" } + new File("${sourceDir}").mkdirs() + into new File("${sourceDir}") +} + +task copyPackages() { + dependsOn unpackBallerinaDistribution + def sourceDir = "${buildDir}/${distributionDir}" + + "/ballerina-${version}-swan-lake/distributions/ballerina-${shortVersion}/repo" + copy { + from "${sourceDir}" + into "${buildDir}/repo" + } +} + +task downloadBalTestProject(type: Download) { + // Download nBallerina latest tag + src "https://github.com/ballerina-platform/nballerina/archive/refs/heads/main.zip" + onlyIfModified true + dest new File("${buildDir}/nballeirna-src.zip") +} + + +task downloadBalFHIRTestProject(type: Download) { + // Download nBallerina latest tag + src "https://github.com/ballerina-platform/module-ballerinax-health.fhir.r4/archive/refs/tags/uscore-v1.0.5.zip" + onlyIfModified true + dest new File("${buildDir}/fhir-src.zip") +} + +task unpackBalTestProject(type: Copy) { + dependsOn downloadBalTestProject + def sourceDir = "${buildDir}/${nbalSourceDir}" + from zipTree { "${buildDir}/nballeirna-src.zip" } + new File("${sourceDir}").mkdirs() + into new File("${sourceDir}") +} + +task unpackBalFHIRTestProject(type: Copy) { + dependsOn downloadBalFHIRTestProject + def sourceDir = "${buildDir}/${fhirSourceDir}" + from zipTree { "${buildDir}/fhir-src.zip" } + new File("${sourceDir}").mkdirs() + into new File("${sourceDir}") +} + +task runLSSimulatorOnnBallerina(type: JavaExec) { + dependsOn copyPackages + dependsOn unpackBalTestProject + + def extractedBalSrcDir = "${buildDir}/${nbalSourceDir}/nballerina-main/compiler" + systemProperty "ls.simulation.src", "${extractedBalSrcDir}" + + systemProperty "ballerina.home", "$buildDir/" + systemProperty "ballerina.version", "${ballerinaLangVersion}" + systemProperty "ls.simulation.duration", "60" + systemProperty "ls.simulation.skipGenerators", System.getProperty("ls.simulation.skipGenerators") + systemProperty "LANG_REPO_BUILD", "false" + + jvmArgs = ['-XX:+HeapDumpOnOutOfMemoryError', "-XX:HeapDumpPath=$rootDir/dump.hprof"] + + maxHeapSize "1536m" + group = "Execution" + description = "Run the main class with JavaExecTask" + classpath = sourceSets.main.runtimeClasspath + main = "org.ballerinalang.langserver.simulator.EditorSimulator" +} + +task runLSSimulatorOnFHIR(type: JavaExec) { + dependsOn copyPackages + dependsOn unpackBalFHIRTestProject + + def extractedBalSrcDir = "${buildDir}/${fhirSourceDir}/module-ballerinax-health.fhir.r4-uscore-v1.0.5/base" + systemProperty "ls.simulation.src", "${extractedBalSrcDir}" + + systemProperty "ballerina.home", "$buildDir/" + systemProperty "ballerina.version", "${ballerinaLangVersion}" + systemProperty "ls.simulation.duration", "60" + systemProperty "ls.simulation.skipGenerators", System.getProperty("ls.simulation.skipGenerators") + systemProperty "LANG_REPO_BUILD", "false" + + jvmArgs = ['-XX:+HeapDumpOnOutOfMemoryError', "-XX:HeapDumpPath=$rootDir/dump.hprof"] + + maxHeapSize "1536m" + group = "Execution" + description = "Run the main class with JavaExecTask" + classpath = sourceSets.main.runtimeClasspath + main = "org.ballerinalang.langserver.simulator.EditorSimulator" +} + +tasks.compileJava { + doFirst { + options.encoding = 'UTF-8' + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} diff --git a/language-server-simulator/gradle.properties b/language-server-simulator/gradle.properties new file mode 100644 index 0000000000..cfad0a361c --- /dev/null +++ b/language-server-simulator/gradle.properties @@ -0,0 +1,5 @@ +eclipseLsp4jVersion=0.15.0 +eclipseLsp4jJsonrpcVersion=0.15.0 +slf4jJdk14Version=1.7.26 +gsonVersion=2.9.1 +slf4jApiVersion=1.7.s26 diff --git a/language-server-simulator/src/main/java/module-info.java b/language-server-simulator/src/main/java/module-info.java new file mode 100644 index 0000000000..2af683a511 --- /dev/null +++ b/language-server-simulator/src/main/java/module-info.java @@ -0,0 +1,12 @@ +module io.ballerina.language.server.simulator { + uses org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator; + requires org.eclipse.lsp4j; + requires io.ballerina.language.server.commons; + requires io.ballerina.language.server.core; + requires org.eclipse.lsp4j.jsonrpc; + requires io.ballerina.lang; + requires io.ballerina.parser; + requires io.ballerina.tools.api; + requires com.google.gson; + requires org.slf4j; +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/Editor.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/Editor.java new file mode 100644 index 0000000000..cc4f436f10 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/Editor.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator; + +import org.ballerinalang.langserver.BallerinaLanguageServer; +import org.ballerinalang.langserver.commons.command.CommandArgument; +import org.ballerinalang.langserver.util.TestUtil; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.jsonrpc.Endpoint; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Represents the editor used by the end user, which editor consists of a set of open tabs. + * + * @since 2201.8.0 + */ +public class Editor { + + private final BallerinaLanguageServer languageServer; + private final Endpoint endpoint; + + private final List tabs = new ArrayList<>(); + private EditorTab activeTab; + + private boolean isPulled = false; + + private Editor(BallerinaLanguageServer languageServer, Endpoint endpoint) { + this.languageServer = languageServer; + this.endpoint = endpoint; + } + + /** + * Simulates opening the editor. Here we initialize the language server. + * + * @return Editor instance + */ + public static Editor open() { + BallerinaLanguageServer languageServer = new BallerinaLanguageServer(); + + EditorOutputStream outputStream = new EditorOutputStream(); + Endpoint endpoint = TestUtil.initializeLanguageSever(languageServer, outputStream); + Editor editor = new Editor(languageServer, endpoint); + outputStream.setEditor(editor); + return editor; + } + + public EditorTab openFile(Path filePath) { + //Pull missing modules from central + if (!isPulled) { + CommandArgument uriArg = CommandArgument.from("doc.uri", filePath); + List args = new ArrayList<>(); + args.add(uriArg); + ExecuteCommandParams params = new ExecuteCommandParams("PULL_MODULE", args); + TestUtil.getExecuteCommandResponse(params, endpoint); + isPulled = true; + } + + EditorTab editorTab = tabs.stream() + .filter(tab -> tab.filePath().equals(filePath)) + .findFirst() + .orElseGet(() -> { + EditorTab tab = new EditorTab(filePath, endpoint, languageServer); + tabs.add(tab); + return tab; + }); + this.activeTab = editorTab; + return editorTab; + } + + public void closeFile(Path filePath) { + Iterator iterator = tabs.iterator(); + while (iterator.hasNext()) { + EditorTab tab = iterator.next(); + if (filePath.equals(tab.filePath())) { + if (activeTab != null && activeTab.equals(tab)) { + activeTab = null; + } + iterator.remove(); + } + } + } + + public void closeTab(EditorTab tab) { + tabs.remove(tab); + if (activeTab != null && activeTab.equals(tab)) { + activeTab = null; + } + } + + public void close() { + this.languageServer.shutdown(); + tabs.forEach(EditorTab::close); + } + + public EditorTab activeTab() { + return activeTab; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorOutputStream.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorOutputStream.java new file mode 100644 index 0000000000..82cb2cee7b --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorOutputStream.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * A custom output stream to consume messages sent from LS to the LS client side. + * + * @since 2201.8.0 + */ +class EditorOutputStream extends ByteArrayOutputStream { + + private static final Logger logger = LoggerFactory.getLogger(EditorOutputStream.class); + + private Editor editor; + + /** + * LSP4J invokes this method after writing a message to the stream. At that point, we should have the complete + * message in the byte array. Here we consume that and reset the array. + * + * @throws IOException IO errors + * @see RemoteEndpoint#request(String, Object) + */ + @Override + public void flush() throws IOException { + String message = this.toString(Charset.defaultCharset()); + reset(); + try { + process(message); + } catch (Throwable t) { + logger.error("Error processing message", t); + } + } + + /** + * Process a received message. We are interested in log message events and telemetry events to identify errors + * occurred. + * + * @param message JSON RPC message received + */ + void process(String message) { + String[] parts = message.replace("\r\n", "\n").split("\n"); + if (parts.length > 1) { + message = parts[parts.length - 1]; + JsonElement jsonMsg = JsonParser.parseString(message); + + if (jsonMsg.isJsonObject()) { + JsonObject obj = jsonMsg.getAsJsonObject(); + String method = obj.get("method").getAsString(); + + switch (method) { + case "telemetry/event": + logger.info("Got telemetry event: {}", obj); + if (editor != null && editor.activeTab() != null) { + logger.info("Current file: {}", editor.activeTab().filePath()); + logger.info("Current file content: \n{}\n========================", + editor.activeTab().textDocument().toString()); + } + break; + case "window/logMessage": + logger.info("Received log message event: {}", obj); + break; + case "textDocument/publishDiagnostics": + // pass + default: + // pass + } + } + } + } + + public void setEditor(Editor editor) { + this.editor = editor; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorSimulator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorSimulator.java new file mode 100644 index 0000000000..967537e4b9 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorSimulator.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator; + +import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.tools.text.LinePosition; +import org.ballerinalang.langserver.simulator.generators.Generators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The main class to simulate the behavior of language server. Similarly to how vscode client use LSP to send different + * updates, this sends similar messages via JSON RPC to the language server. + * + * @since 2201.8.0 + */ +public class EditorSimulator { + + private static final Logger logger = LoggerFactory.getLogger(EditorSimulator.class); + + private static final String PROP_DURATION = "ls.simulation.duration"; + public static final String PROP_SOURCE_DIR = "ls.simulation.src"; + + private static final SecureRandom random = new SecureRandom(); + + public static void main(String[] args) throws IOException { + try { + run(); + } catch (Exception e) { + logger.error("Error occurred while running the simulator", e); + throw e; + } + } + + public static void run() throws IOException { + int durationSeconds = Integer.parseInt(System.getProperty(PROP_DURATION, "60")) * 60; + String projectPath = System.getProperty(PROP_SOURCE_DIR); + if (projectPath == null) { + throw new IllegalArgumentException("No ballerina project path provided"); + } + + Path path = Paths.get(projectPath); + logger.info("Using project: {}, path: {}", path.toString(), projectPath); + + List balFiles = Files.list(path) + .filter(Files::isRegularFile) + .filter(p -> p.getFileName() != null) + .filter(p -> p.getFileName().toString().endsWith(".bal")) + .collect(Collectors.toList()); + + if (balFiles.isEmpty()) { + throw new IllegalArgumentException("No bal files found in the provided directory"); + } + + Path modulesPath = path.resolve("modules"); + if (Files.exists(modulesPath)) { + Files.list(modulesPath) + .filter(Files::isDirectory) + .flatMap(modPath -> { + try { + return Files.list(modPath) + .filter(Files::isRegularFile) + .filter(p -> p.getFileName() != null) + .filter(p -> p.getFileName().toString().endsWith(".bal")); + } catch (IOException e) { + logger.error("Unable to read path: {}", modPath); + return Stream.empty(); + } + }) + .forEach(balFiles::add); + } + + logger.info("Found bal files in project: {}", balFiles.stream() + .map(Path::toString).collect(Collectors.joining("\n"))); + + Editor editor = Editor.open(); + Runtime.getRuntime().addShutdownHook(new Thread(editor::close)); + + long endTime = Instant.now().getEpochSecond() + durationSeconds; + while (Instant.now().getEpochSecond() < endTime) { + int i = random.nextInt(balFiles.size()); + Path balFile = balFiles.get(i); + EditorTab editorTab = editor.openFile(balFile); + + logger.info("Generating random code snippet"); + // Get random generator type + Generators.Type type = getRandomGenerator(); + logger.info("Generating snippet of type: {}", type); + String content = Generators.generate(type); + + if (type == Generators.Type.IMPORT_STATEMENT) { + // Set cursor to start of the file + editorTab.cursor(0, 0); + } else { + // Select a random place to type random code + ModulePartNode modulePartNode = editorTab.syntaxTree().rootNode(); + NodeList members = modulePartNode.members(); + ModuleMemberDeclarationNode moduleMemberDeclarationNode = members.get(random.nextInt(members.size())); + LinePosition linePosition = moduleMemberDeclarationNode.location().lineRange().startLine(); + // Set cursor to start of random node + editorTab.cursor(linePosition.line(), linePosition.offset()); + } + + logger.info("Typing in editor tab: {} -> {}", editorTab, content); + CompletableFuture future = CompletableFuture.runAsync(() -> { + editorTab.type(content); + editorTab.completions(); + }); + + // While the snippet is being typed, check if we have reached a timeout + while (!future.isDone() && Instant.now().getEpochSecond() < endTime) { + logger.info("Remaining time: {}", endTime - Instant.now().getEpochSecond()); + try { + Thread.sleep(60 * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted editing", e); + break; + } + } + + try { + int sleepSecs = 1 + random.nextInt(5); + Thread.sleep(sleepSecs * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted simulation", e); + break; + } + } + + logger.info("Exiting..."); + editor.close(); + System.exit(0); + } + + /** + * Generate a random syntax tree node (top level) to be inserted to the source document. + * + * @return Source for a random top level node. + */ + public static Generators.Type getRandomGenerator() { + List types = Arrays.stream(Generators.Type.values()) + .filter(Generators.Type::isTopLevelNode) + .collect(Collectors.toList()); + + // Get random generator + return types.get(random.nextInt(types.size())); + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorTab.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorTab.java new file mode 100644 index 0000000000..ca69e276d7 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/EditorTab.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.ballerina.compiler.syntax.tree.SyntaxTree; +import io.ballerina.projects.Document; +import io.ballerina.tools.text.LinePosition; +import io.ballerina.tools.text.TextDocument; +import io.ballerina.tools.text.TextDocumentChange; +import io.ballerina.tools.text.TextDocuments; +import io.ballerina.tools.text.TextEdit; +import io.ballerina.tools.text.TextRange; +import org.ballerinalang.langserver.BallerinaLanguageServer; +import org.ballerinalang.langserver.util.TestUtil; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Represents a tab in the {@link Editor}. Simulates the behavior of cursor and current text in the document. + * + * @since 2201.8.0 + */ +public class EditorTab { + + private static final Logger logger = LoggerFactory.getLogger(EditorTab.class); + + private final Path filePath; + private final Endpoint endpoint; + private final BallerinaLanguageServer languageServer; + + private TextDocument textDocument; + private Position cursor; + + private final SecureRandom random = new SecureRandom(); + private final PrintWriter writer = new PrintWriter(System.out, true, Charset.defaultCharset()); + + public EditorTab(Path filePath, Endpoint endpoint, BallerinaLanguageServer languageServer) { + this.filePath = filePath; + this.endpoint = endpoint; + this.languageServer = languageServer; + try { + String content = Files.readString(filePath); + this.textDocument = TextDocuments.from(content); + logger.info("Opening document: {}", filePath); + TestUtil.openDocument(endpoint, filePath); + LinePosition linePosition = textDocument.linePositionFrom(content.length() - 1); + cursor(linePosition.line(), linePosition.offset()); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Simulates a user typing the provided content in the editor. Content is typed character by character similarly to + * how a user does it. + * + * @param content Text content to be typed in the editor. + */ + public void type(String content) { + int missCount = 0; + for (int i = 0; i < content.length(); i++) { + String typedChar = Character.toString(content.charAt(i)); + + int startOffset = textDocument.textPositionFrom(LinePosition.from(cursor.getLine(), cursor.getCharacter())); + TextEdit edit = TextEdit.from(TextRange.from(startOffset, 0), typedChar); + TextDocumentChange change = TextDocumentChange.from(new TextEdit[]{edit}); + textDocument = textDocument.apply(change); + + LinePosition newLinePos = textDocument.linePositionFrom(startOffset + 1); + try { + TestUtil.didChangeDocument(this.endpoint, this.filePath, textDocument.toString()); + } catch (Throwable t) { + logger.error("Caught error in didChange", t); + } + cursor(newLinePos.line(), newLinePos.offset()); + + if (i % 10 == 0) { + float completionPercentage = ((float) i / (float) content.length()) * 100; + writer.printf("%.1f%%\r", completionPercentage); + } + + // Get completions in the background + if (i % 3 == 0) { + CompletableFuture.runAsync(this::completions); + CompletableFuture.runAsync(this::codeActions); + } + + if (isDocumentNotInSync()) { + missCount++; + } + + try { + Thread.sleep(100 + (long) random.nextInt(300)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Interrupted", e); + break; + } + } + logger.info("Typed provided content in file: {} -> \n{}", + filePath, content.substring(0, Math.min(20, content.length()))); + logger.info("Typed {} characters with {} out of sync scenarios", content.length(), missCount); + + while (isDocumentNotInSync()) { + logger.info("Document out of sync. Waiting 30 seconds and syncing..."); + try { + Thread.sleep(30 * (long) 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + TestUtil.didChangeDocument(this.endpoint, this.filePath, textDocument.toString()); + } + } + + /** + * Check if the document in this instance is similar to that is in the language server. + * + * @return True if the document content is not equal to that in workspace manager + */ + private boolean isDocumentNotInSync() { + Optional document = languageServer.getWorkspaceManager().document(filePath); + if (document.isPresent()) { + return !document.get().textDocument().toString().equals(textDocument.toString()); + } else { + logger.warn("Document not found in workspace manager: {}", filePath); + } + + return true; + } + + /** + * Get completions for the current cursor position. + */ + public void completions() { + String completionResponse = TestUtil.getCompletionResponse(filePath.toString(), cursor, endpoint, ""); + JsonObject json = JsonParser.parseString(completionResponse).getAsJsonObject(); + boolean hasError = false; + String resultProp = "result"; + if (json.has(resultProp) && json.get(resultProp).isJsonObject()) { + JsonObject result = json.getAsJsonObject(resultProp); + if (!result.has("left") || !result.get("left").isJsonArray()) { + hasError = true; + } + } else { + hasError = true; + } + + if (hasError) { + logger.warn("Completion request unsuccessful! cursor: {} -> {}", filePath, cursor); + } + } + + /** + * Get code actions for the current cursor position. + */ + public void codeActions() { + CodeActionContext codeActionContext = new CodeActionContext(Collections.emptyList()); + Range range = new Range(cursor, cursor); + TestUtil.getCodeActionResponse(endpoint, filePath.toString(), range, codeActionContext); + } + + public void cursor(int line, int offset) { + this.cursor = new Position(line, offset); + } + + public Position cursor() { + return this.cursor; + } + + public SyntaxTree syntaxTree() { + return SyntaxTree.from(textDocument); + } + + public TextDocument textDocument() { + return textDocument; + } + + public Path filePath() { + return filePath; + } + + public void close() { + logger.info("Closing document: {}", filePath()); + TestUtil.closeDocument(endpoint, filePath()); + } + + @Override + public String toString() { + return "EditorTab{" + + "filePath=" + filePath + + ", cursor=(" + cursor.getLine() + ", " + cursor.getCharacter() + ")" + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EditorTab editorTab = (EditorTab) o; + return Objects.equals(filePath, editorTab.filePath); + } + + @Override + public int hashCode() { + return Objects.hash(filePath); + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ClassGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ClassGenerator.java new file mode 100644 index 0000000000..e176d36f80 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ClassGenerator.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * Class code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class ClassGenerator extends CodeSnippetGenerator { + + @Override + public String generate() { + return generateRandomClass(); + } + + @Override + public Generators.Type type() { + return Generators.Type.CLASS; + } + + private String generateRandomClass() { + FunctionGenerator functionGenerator = Generators.getGenerator(Generators.Type.FUNCTION); + + int numOfFunctions = 1 + random.nextInt(100); + String body = IntStream.range(0, numOfFunctions) + .mapToObj(i -> functionGenerator.generateRandomFunction("fn" + i, "string")) + .collect(Collectors.joining("\n")); + return "class AClass {\n" + + " " + body + "\n" + + "}"; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/CodeSnippetGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/CodeSnippetGenerator.java new file mode 100644 index 0000000000..97eec1ba36 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/CodeSnippetGenerator.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import java.security.SecureRandom; +import java.util.List; + +/** + * Abstract implementation of code snippet generator for the LS simulator. + * + * @since 2201.8.0 + */ +public abstract class CodeSnippetGenerator { + + protected final List primitiveTypes = List.of("string", "int", "float", "decimal", "boolean"); + protected SecureRandom random = new SecureRandom(); + + public abstract String generate(); + + public abstract Generators.Type type(); +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/FunctionGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/FunctionGenerator.java new file mode 100644 index 0000000000..41a5ebd030 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/FunctionGenerator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +/** + * Function code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class FunctionGenerator extends CodeSnippetGenerator { + + @Override + public String generate() { + return generateRandomFunction(); + } + + @Override + public Generators.Type type() { + return Generators.Type.FUNCTION; + } + + public String generateRandomFunction() { + String name = "fn"; + String returnType = "string"; + return generateRandomFunction(name, returnType); + } + + public String generateRandomFunction(String name, String returnType) { + return "\npublic function " + name + "() returns " + returnType + " {\n" + + " " + getRandomFunctionBody(returnType) + "\n" + + "}\n"; + } + + public String getRandomFunctionBody(String returnType) { + StatementGenerator statementGenerator = Generators.getGenerator(Generators.Type.STATEMENT); + String body = ""; + body += "\t" + statementGenerator.getRandomStatement(); + body += "\t" + statementGenerator.getRandomStatement(); + body += "\t" + statementGenerator.getRandomStatement(); + body += "\treturn " + returnType + ";"; + return body; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/Generators.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/Generators.java new file mode 100644 index 0000000000..f0865c11f8 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/Generators.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumMap; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Factory to access {@link CodeSnippetGenerator}s. + * + * @since 2201.8.0 + */ +public class Generators { + + private static final Logger logger = LoggerFactory.getLogger(Generators.class); + private static final String PROP_SKIPPED_GENERATORS = "ls.simulation.skipGenerators"; + private static final Generators instance = new Generators(); + private final EnumMap GENERATORS; + + private Generators() { + // Get skipped generators + String property = System.getProperty(PROP_SKIPPED_GENERATORS, ""); + Set skippedGenerators = Stream.of(property.split(",")) + .filter(type -> !type.isBlank()) + .map(Type::valueOf) + .collect(Collectors.toSet()); + logger.info("Skipping generators of type: " + skippedGenerators); + + // Load generators + GENERATORS = new EnumMap<>(Generators.Type.class); + ServiceLoader.load(CodeSnippetGenerator.class) + .forEach(generator -> { + if (!skippedGenerators.contains(generator.type())) { + GENERATORS.put(generator.type(), generator); + } + }); + } + + /** + * Generate a code snippet of provided type. + * + * @param type Type of the required code snippet. + * @return Generated code snippet. + */ + public static String generate(Type type) { + if (getInstance().GENERATORS.containsKey(type)) { + return instance.GENERATORS.get(type).generate(); + } + + return ""; + } + + public static T getGenerator(Type type) { + return (T) instance.GENERATORS.get(type); + } + + public static Generators getInstance() { + return instance; + } + + /** + * Different types of code snippets which can be generated. + */ + public enum Type { + FUNCTION(true), + CLASS(true), + SERVICE(true), + TYPE_DEFINITION(true), + STATEMENT(false), + MATCH_STATEMENT(false), + VARIABLE_DECLARATION_STATEMENT(true), + IMPORT_STATEMENT(true); + + private final boolean topLevelNode; + + Type(boolean topLevelNode) { + this.topLevelNode = topLevelNode; + } + + public boolean isTopLevelNode() { + return topLevelNode; + } + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ImportStatementGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ImportStatementGenerator.java new file mode 100644 index 0000000000..b706507962 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ImportStatementGenerator.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; +import org.ballerinalang.langserver.simulator.EditorSimulator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Import statement snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class ImportStatementGenerator extends CodeSnippetGenerator { + + private static final String PACKAGE_NAME = "nballerina"; + + @Override + public String generate() { + //Look for modules in the source and generate import statements for them. + String projectPath = System.getProperty(EditorSimulator.PROP_SOURCE_DIR); + if (projectPath == null) { + return ""; + } + Path path = Paths.get(projectPath); + Path modulesPath = path.resolve("modules"); + if (Files.exists(modulesPath)) { + try (Stream paths = Files.list(modulesPath)) { + List imports = paths.filter(Files::isDirectory) + .map(p -> "import " + PACKAGE_NAME + "." + p.getFileName() + ";") + .collect(Collectors.toList()); + if (!imports.isEmpty()) { + return imports.get(random.nextInt(imports.size())); + } + } catch (IOException e) { + //ignore + } + } + return ""; + } + + @Override + public Generators.Type type() { + return Generators.Type.IMPORT_STATEMENT; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/MatchStatementGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/MatchStatementGenerator.java new file mode 100644 index 0000000000..34d20c7b0e --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/MatchStatementGenerator.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +/** + * Match statement code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class MatchStatementGenerator extends CodeSnippetGenerator { + + /** + * Generates a match statement. + * + * @return Match statement + */ + @Override + public String generate() { + // This statement has intentionally added syntax errors. + return "\nmatch t {\n" + + " () => {\n}" + + " [1, 2] => {\n}" + + " [1, 2, 10] => {\n}" + + " [int => {\n" + + " _ => {\n}" + + "}\n"; + } + + @Override + public Generators.Type type() { + return Generators.Type.MATCH_STATEMENT; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ServiceGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ServiceGenerator.java new file mode 100644 index 0000000000..76cb513ad9 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/ServiceGenerator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +/** + * Service code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class ServiceGenerator extends CodeSnippetGenerator { + + @Override + public String generate() { + return generateRandomService(); + } + + @Override + public Generators.Type type() { + return Generators.Type.SERVICE; + } + + public String generateRandomService() { + return "\nservice /context1 on new http:Listener(8080) {\n" + + " resource function get path1(http:Caller caller, http:Request req) {\n" + + " \n" + + " }\n" + + "}\n"; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/StatementGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/StatementGenerator.java new file mode 100644 index 0000000000..31e4093695 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/StatementGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +/** + * Statement code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class StatementGenerator extends CodeSnippetGenerator { + + @Override + public String generate() { + return getRandomStatement(); + } + + @Override + public Generators.Type type() { + return Generators.Type.STATEMENT; + } + + public String getRandomStatement() { + switch (random.nextInt(2)) { + case 0: + return Generators.generate(Generators.Type.MATCH_STATEMENT); + case 1: + default: + return Generators.generate(Generators.Type.VARIABLE_DECLARATION_STATEMENT); + } + } + +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/TypeDefinitionGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/TypeDefinitionGenerator.java new file mode 100644 index 0000000000..07d20d2893 --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/TypeDefinitionGenerator.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Type definition code snippet generator. + * + * @since 2201.8.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class TypeDefinitionGenerator extends CodeSnippetGenerator { + + private final List generatedTypeDefNames = new ArrayList<>(); + private int typeCount = 0; + + @Override + public String generate() { + switch (random.nextInt(3)) { + case 1: + return generateUnionType(); + case 2: + return generateRecordType(); + case 3: + default: + return generateObjectTypeDef(); + } + } + + public String generateRecordType() { + String typeName = "Rec" + typeCount; + + List fields = new ArrayList<>(); + for (int i = 0; i < 2 + random.nextInt(100); i++) { + String field; + if (random.nextBoolean() || generatedTypeDefNames.isEmpty()) { + field = String.format("\t%s field%d;", primitiveTypes.get(random.nextInt(primitiveTypes.size())), i); + } else { + field = String.format("\t%s field%d;", + generatedTypeDefNames.get(random.nextInt(generatedTypeDefNames.size())), i); + } + fields.add(field); + } + + typeCount++; + generatedTypeDefNames.add(typeName); + return String.format("%ntype %s {|%n%s%n|};%n", typeName, String.join("\n", fields)); + } + + public String generateUnionType() { + // Member types + Set memberTypes = new HashSet<>(); + for (int i = 0; i < 2 + random.nextInt(3); i++) { + String memberType; + do { + if (random.nextBoolean() || generatedTypeDefNames.isEmpty()) { + memberType = primitiveTypes.get(random.nextInt(primitiveTypes.size())); + } else { + memberType = generatedTypeDefNames.get(random.nextInt(generatedTypeDefNames.size())); + } + } while (memberTypes.contains(memberType)); + memberTypes.add(memberType); + } + + String typeName = "Type" + typeCount; + typeCount++; + generatedTypeDefNames.add(typeName); + return String.format("%ntype %s %s;%n", typeName, String.join(" | ", memberTypes)); + } + + public String generateObjectTypeDef() { + String typeName = "ObjectDef" + typeCount; + typeCount++; + generatedTypeDefNames.add(typeName); + return "\ntype " + typeName + " object {\n" + + "\n\tpublic function doSomething() returns UnkType;\n" + + "};\n"; + } + + @Override + public Generators.Type type() { + return Generators.Type.TYPE_DEFINITION; + } +} diff --git a/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/VarDeclarationStatementGenerator.java b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/VarDeclarationStatementGenerator.java new file mode 100644 index 0000000000..5eaf6a41da --- /dev/null +++ b/language-server-simulator/src/main/java/org/ballerinalang/langserver/simulator/generators/VarDeclarationStatementGenerator.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://wso2.com) All Rights Reserved. + * + * Licensed 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 org.ballerinalang.langserver.simulator.generators; + +import org.ballerinalang.annotation.JavaSPIService; + +/** + * Variable declaration code snippet generator. + * + * @since 2.0.0 + */ +@JavaSPIService("org.ballerinalang.langserver.org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator") +public class VarDeclarationStatementGenerator extends CodeSnippetGenerator { + + private int varCount = 0; + + public String generate() { + varCount++; + return String.format("%n%s %s = createVar();%n", + primitiveTypes.get(random.nextInt(primitiveTypes.size())), "myVar" + varCount); + } + + @Override + public Generators.Type type() { + return Generators.Type.VARIABLE_DECLARATION_STATEMENT; + } +} diff --git a/language-server-simulator/src/main/resources/META-INF/services/org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator b/language-server-simulator/src/main/resources/META-INF/services/org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator new file mode 100644 index 0000000000..003b872e40 --- /dev/null +++ b/language-server-simulator/src/main/resources/META-INF/services/org.ballerinalang.langserver.simulator.generators.CodeSnippetGenerator @@ -0,0 +1,8 @@ +org.ballerinalang.langserver.simulator.generators.VarDeclarationStatementGenerator +org.ballerinalang.langserver.simulator.generators.TypeDefinitionGenerator +org.ballerinalang.langserver.simulator.generators.StatementGenerator +org.ballerinalang.langserver.simulator.generators.ImportStatementGenerator +org.ballerinalang.langserver.simulator.generators.ServiceGenerator +org.ballerinalang.langserver.simulator.generators.ClassGenerator +org.ballerinalang.langserver.simulator.generators.MatchStatementGenerator +org.ballerinalang.langserver.simulator.generators.FunctionGenerator diff --git a/settings.gradle b/settings.gradle index c4c06eaa4c..9b751d9b82 100644 --- a/settings.gradle +++ b/settings.gradle @@ -41,4 +41,5 @@ gradleEnterprise { termsOfServiceAgree = 'yes' } } +include 'language-server-simulator'