diff --git a/LICENSE.md b/LICENSE.md index f49a4e1..9a8fc67 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2018 Nadeeshaan Gunasinghe Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/.vscodeignore b/client/.vscodeignore new file mode 100644 index 0000000..d3f0107 --- /dev/null +++ b/client/.vscodeignore @@ -0,0 +1,8 @@ +.vscode/** +typings/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..b63d732 --- /dev/null +++ b/client/package.json @@ -0,0 +1,53 @@ +{ + "name": "swagger-language-sever-extension", + "description": "VSCode Client extension for Swagger", + "author": "Nadeeshaan", + "license": "MIT", + "version": "0.0.1", + "publisher": "nadeeshaan", + "engines": { + "vscode": "^1.26.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onLanguage:yaml" + ], + "main": "./out/extension", + "contributes": { + "configuration": { + "type": "object", + "title": "Example configuration", + "properties": { + "languageServerExample.maxNumberOfProblems": { + "scope": "resource", + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server." + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "build:grammar": "tsc -p syntaxes/build && node syntaxes/build/build.js", + "test": "npm run compile && node ./node_modules/vscode/bin/test", + "clean": "rimraf server-build target", + "package": "npm run compile && vsce package" + }, + "devDependencies": { + "vscode": "^1.1.5", + "@types/node": "^8.10.25", + "tslint": "^5.8.0", + "typescript": "^2.6.1", + "vsce": "^1.36.2" + }, + "dependencies": { + "vscode": "^1.1.21", + "vscode-languageclient": "^4.1.4" + } +} diff --git a/client/src/extension.ts b/client/src/extension.ts new file mode 100644 index 0000000..7d4be73 --- /dev/null +++ b/client/src/extension.ts @@ -0,0 +1,43 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as path from 'path'; + +import { workspace, Disposable, ExtensionContext } from 'vscode'; +import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient'; + +const main: string = 'StdioLauncher'; + +export function activate(context: ExtensionContext) { + + const { JAVA_HOME } = process.env; + console.log(`Using java from JAVA_HOME: ${JAVA_HOME}`); + let excecutable : string = path.join(JAVA_HOME, 'bin', 'java'); + + let jarPath = path.join(__dirname, '..', 'launcher', 'ls-launcher.jar'); + const args: string[] = ['-cp', jarPath]; + args.push('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005,quiet=y'); + console.log(jarPath); + + let serverOptions: ServerOptions = { + command: excecutable, + args: [...args, main], + options: {} + }; + + // Options to control the language client + let clientOptions: LanguageClientOptions = { + // Register the server for plain text documents + documentSelector: [{scheme: 'file', language: 'yaml'}] + }; + + // Create the language client and start the client. + let disposable = new LanguageClient('languageServerExample', 'Language Server Example', serverOptions, clientOptions).start(); + + // Push the disposable to the context's subscriptions so that the + // client can be deactivated on extension deactivation + context.subscriptions.push(disposable); +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..4e6b0ca --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "out", + "lib": [ "es2016" ], + "sourceMap": true + }, + "exclude": [ + "node_modules", + "server" + ] +} \ No newline at end of file diff --git a/langserver-core/pom.xml b/langserver-core/pom.xml new file mode 100644 index 0000000..cf82ea6 --- /dev/null +++ b/langserver-core/pom.xml @@ -0,0 +1,31 @@ + + + + + swagger-language-server + org.swagger.ls + 0.0.1-SNAPSHOT + + 4.0.0 + + langserver-core + jar + \ No newline at end of file diff --git a/langserver-core/src/main/java/org/swagger/langserver/DocumentManager.java b/langserver-core/src/main/java/org/swagger/langserver/DocumentManager.java new file mode 100644 index 0000000..8b78b6d --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/DocumentManager.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver; + +import java.nio.file.Path; + +/** + * Document Manager is responsible for maintaining the content of the documents. + */ +public interface DocumentManager { + /** + * Checks whether the given file is open in workspace. + * + * @param filePath Path of the file + * @return True if the given file is open + */ + boolean isFileOpen(Path filePath); + + /** + * Opens the given file in document manager. + * + * @param filePath Path of the file + * @param content Content of the file + */ + void openFile(Path filePath, String content); + + /** + * Updates given file in document manager with new content. + * + * @param filePath Path of the file + * @param updatedContent New content of the file + */ + void updateFile(Path filePath, String updatedContent); + + /** + * Close the given file in document manager. + * + * @param filePath Path of the file + */ + void closeFile(Path filePath); + + /** + * Gets uptodate content of the file. + * + * @param filePath Path of the file + * @return Content of the file + */ + String getFileContent(Path filePath); +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/DocumentManagerImpl.java b/langserver-core/src/main/java/org/swagger/langserver/DocumentManagerImpl.java new file mode 100644 index 0000000..d18380d --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/DocumentManagerImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Implementation of DocumentManager Interface. + */ +public class DocumentManagerImpl implements DocumentManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(DocumentManagerImpl.class); + + private ConcurrentHashMap documents; + + private DocumentManagerImpl() { + this.documents = new ConcurrentHashMap<>(); + } + + private static class InnerSingleton { + private static final DocumentManagerImpl INSTANCE = new DocumentManagerImpl(); + } + + public static DocumentManagerImpl getInstance() { + return InnerSingleton.INSTANCE; + } + + /** + * Checks whether the given file is open in workspace. + * + * @param filePath Path of the file + * @return Returns the list of opened document paths + */ + @Override + public boolean isFileOpen(Path filePath) { + return getPathEntry(filePath) != null; + } + + /** + * Opens the given file in document manager. + * + * @param filePath Path of the file + * @param content Content of the file + */ + @Override + public void openFile(Path filePath, String content) { + if (isFileOpen(filePath)) { + LOGGER.warn("File is Already opened"); + return; + } + this.documents.put(filePath, content); + } + + /** + * Updates given file in document manager with new content. + * + * @param filePath Path of the file + * @param updatedContent New content of the file + */ + @Override + public void updateFile(Path filePath, String updatedContent) { + Path opened = getPathEntry(filePath); + if (opened == null) { + LOGGER.error("Cannot find the file to update: " + filePath.toString()); + return; + } + + this.documents.put(opened, updatedContent); + } + + /** + * Close the given file in document manager. + * + * @param filePath Path of the file + */ + @Override + public void closeFile(Path filePath) { + Path opened = getPathEntry(filePath); + if (opened == null) { + LOGGER.warn("Cannot find open file [" + filePath.toString() + "] to close"); + return; + } + + this.documents.remove(opened); + } + + /** + * Gets uptodate content of the file. + * + * @param filePath Path of the file + * @return Content of the file + */ + @Override + public String getFileContent(Path filePath) { + if (!isFileOpen(filePath)) { + return null; + } + return documents.get(filePath); + } + + // Private methods + + /** + * Get the path entry for the given file path. + * + * @param filePath File Path to se + * @return {@link Path} Path Entry + */ + private Path getPathEntry(Path filePath) { + return this.documents.entrySet().stream().filter(entry -> { + try { + return Files.isSameFile(entry.getKey(), filePath); + } catch (IOException e) { + LOGGER.error("Error locating File: " + e.getMessage()); + return false; + } + }).map(Map.Entry::getKey).findFirst().orElse(null); + } +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/SwaggerLanguageServer.java b/langserver-core/src/main/java/org/swagger/langserver/SwaggerLanguageServer.java new file mode 100644 index 0000000..9960194 --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/SwaggerLanguageServer.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver; + +import org.eclipse.lsp4j.CompletionOptions; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageClientAware; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; + +import java.util.concurrent.CompletableFuture; + +/** + * Language Server Implementation for Swagger. + */ +public class SwaggerLanguageServer implements LanguageServer, LanguageClientAware { + + private TextDocumentService textDocumentService; + + private WorkspaceService workspaceService; + + private LanguageClient client; + + public SwaggerLanguageServer() { + this.textDocumentService = new SwaggerTextDocumentService(); + this.workspaceService = new SwaggerWorkspaceService(); + } + + @Override + public void connect(LanguageClient languageClient) { + this.client = languageClient; + } + + @Override + public CompletableFuture initialize(InitializeParams initializeParams) { + final InitializeResult initializeResult = new InitializeResult(new ServerCapabilities()); + initializeResult.getCapabilities().setTextDocumentSync(TextDocumentSyncKind.Full); + initializeResult.getCapabilities().setCompletionProvider(new CompletionOptions()); + return CompletableFuture.supplyAsync(() -> initializeResult); + } + + @Override + public CompletableFuture shutdown() { + return null; + } + + @Override + public void exit() { + + } + + @Override + public TextDocumentService getTextDocumentService() { + return this.textDocumentService; + } + + @Override + public WorkspaceService getWorkspaceService() { + return this.workspaceService; + } +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/SwaggerTextDocumentService.java b/langserver-core/src/main/java/org/swagger/langserver/SwaggerTextDocumentService.java new file mode 100644 index 0000000..816ff7e --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/SwaggerTextDocumentService.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver; + +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.swagger.langserver.completion.ContentParserUtil; + +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Swagger Text Document Service. + */ +public class SwaggerTextDocumentService implements TextDocumentService { + + private DocumentManager documentManager; + + public SwaggerTextDocumentService() { + this.documentManager = DocumentManagerImpl.getInstance(); + } + + @Override + public CompletableFuture, CompletionList>>completion( + CompletionParams completionParams) { + return CompletableFuture.supplyAsync(() -> { + List completionItems; + try { + Path path = Paths.get(new URI(completionParams.getTextDocument().getUri())); + Position position = completionParams.getPosition(); + completionItems = ContentParserUtil.getCompletions(documentManager.getFileContent(path), position); + } catch (URISyntaxException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + completionItems = new ArrayList<>(); + } + return Either.forLeft(completionItems); + }); + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem completionItem) { + return null; + } + + @Override + public CompletableFuture hover(TextDocumentPositionParams textDocumentPositionParams) { + return null; + } + + @Override + public CompletableFuture signatureHelp(TextDocumentPositionParams textDocumentPositionParams) { + return null; + } + + @Override + public CompletableFuture> definition(TextDocumentPositionParams + textDocumentPositionParams) { + return null; + } + + @Override + public CompletableFuture> references(ReferenceParams referenceParams) { + return null; + } + + @Override + public CompletableFuture> documentHighlight + (TextDocumentPositionParams textDocumentPositionParams) { + return null; + } + + @Override + public CompletableFuture> documentSymbol(DocumentSymbolParams + documentSymbolParams) { + return null; + } + + @Override + public CompletableFuture> codeAction(CodeActionParams codeActionParams) { + return null; + } + + @Override + public CompletableFuture> codeLens(CodeLensParams codeLensParams) { + return null; + } + + @Override + public CompletableFuture resolveCodeLens(CodeLens codeLens) { + return null; + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams documentFormattingParams) { + return null; + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams + documentRangeFormattingParams) { + return null; + } + + @Override + public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams + documentOnTypeFormattingParams) { + return null; + } + + @Override + public CompletableFuture rename(RenameParams renameParams) { + return null; + } + + @Override + public void didOpen(DidOpenTextDocumentParams didOpenTextDocumentParams) { + String uri = didOpenTextDocumentParams.getTextDocument().getUri(); + String content = didOpenTextDocumentParams.getTextDocument().getText(); + this.documentManager.openFile(Paths.get(URI.create(uri)), content); + } + + @Override + public void didChange(DidChangeTextDocumentParams didChangeTextDocumentParams) { + String uri = didChangeTextDocumentParams.getTextDocument().getUri(); + String content = didChangeTextDocumentParams.getContentChanges().get(0).getText(); + this.documentManager.updateFile(Paths.get(URI.create(uri)), content); + } + + @Override + public void didClose(DidCloseTextDocumentParams didCloseTextDocumentParams) { + String uri = didCloseTextDocumentParams.getTextDocument().getUri(); + this.documentManager.closeFile(Paths.get(URI.create(uri))); + } + + @Override + public void didSave(DidSaveTextDocumentParams didSaveTextDocumentParams) { + } +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/SwaggerWorkspaceService.java b/langserver-core/src/main/java/org/swagger/langserver/SwaggerWorkspaceService.java new file mode 100644 index 0000000..337a825 --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/SwaggerWorkspaceService.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver; + +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.services.WorkspaceService; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Implementation of Swagger Workspace Service. + */ +public class SwaggerWorkspaceService implements WorkspaceService { + @Override + public CompletableFuture executeCommand(ExecuteCommandParams params) { + return null; + } + + @Override + public CompletableFuture> symbol(WorkspaceSymbolParams + workspaceSymbolParams) { + return null; + } + + @Override + public void didChangeConfiguration(DidChangeConfigurationParams didChangeConfigurationParams) { + + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams didChangeWatchedFilesParams) { + + } + + @Override + public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { + + } +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/completion/ContentParserUtil.java b/langserver-core/src/main/java/org/swagger/langserver/completion/ContentParserUtil.java new file mode 100644 index 0000000..4a99291 --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/completion/ContentParserUtil.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver.completion; + +import io.swagger.models.Swagger; +import io.swagger.parser.SwaggerParser; +import io.swagger.parser.util.SwaggerDeserializationResult; +import joptsimple.internal.Strings; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.Position; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Utilities to parse content. + */ +public class ContentParserUtil { + + private ContentParserUtil() { + } + + private static final String LINE_SEPARATOR = System.lineSeparator(); + + private static Iterable getYAMLNodeTree(String content) { + Constructor constructor = new Constructor(); + Yaml yaml = new Yaml(constructor); + return yaml.composeAll(new InputStreamReader(new ByteArrayInputStream(content.getBytes()))); + } + + /** + * Mapping node where the cursor belongs to. + * + * @param line Cursor Line + * @param content Document Content to Parse + */ + public static void getMappingNodeForCursor(int line, String content) { + Iterable nodes = getYAMLNodeTree(content); + nodes.forEach(node -> { + if (node instanceof MappingNode) { + ((MappingNode) node).getValue().forEach(nodeTuple -> { + // TODO: Need Implementation + }); + } + }); + } + + /** + * Get the modified document content. + * + * Note: Here replace the line content at the cursor with spaces to avoid parser issues + * + * @param fileUri Document uri + * @param cursorLine Current cursor line + * @return {@link String} Modified content + * @throws IOException IOException if the file read fails + */ + public String modifyContent(String fileUri, int cursorLine) throws IOException { + String content = new String(Files.readAllBytes(Paths.get(URI.create(fileUri)))); + String[] lines = content.split("\\r?\\n"); + lines[cursorLine] = lines[cursorLine].replaceAll("\\w", " "); + return Strings.join(lines, LINE_SEPARATOR); + } + + public static List getCompletions(String content, Position position) throws NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + Deque fieldStack = new ArrayDeque<>(); + SwaggerDeserializationResult swaggerDeserializationResult = new SwaggerParser().readWithInfo(content); + Constructor constructor = new Constructor(); + Yaml yaml = new Yaml(constructor); + Iterable iterable = yaml.composeAll(new InputStreamReader(new ByteArrayInputStream(content.getBytes()))); + iterable.forEach(o -> { + FieldIdentifier fieldIdentifier = new FieldIdentifier(); + fieldIdentifier + .calculateFieldStack(((MappingNode) o).getValue(), position.getLine(), position.getCharacter(), 0); + if (!fieldIdentifier.getFieldStack().isEmpty()) { + fieldStack.addAll(fieldIdentifier.getFieldStack()); + } + }); + Swagger swagger = swaggerDeserializationResult.getSwagger(); + List fields = new ArrayList<>(fieldStack); + Collections.reverse(fields); + List completions = getCompletionFields(swagger, fields); + return completions.stream().map(field -> { + CompletionItem completionItem = new CompletionItem(); + completionItem.setInsertText(field); + completionItem.setLabel(field); + completionItem.setKind(CompletionItemKind.Field); + completionItem.setDetail(field); + return completionItem; + }).collect(Collectors.toList()); + } + + private static List getCompletionFields(Swagger swagger, List fieldStack) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class cls = swagger.getClass(); + Object invokeAgainst = swagger; + + for (int i = 0; i < fieldStack.size(); i++) { + String field = fieldStack.get(i); + Method m = cls.getMethod("get" + field.substring(0, 1).toUpperCase() + field.substring(1).toLowerCase()); + cls = m.getReturnType(); + + if (cls.equals(Map.class)) { + LinkedHashMap invokeResult = (LinkedHashMap) m.invoke(invokeAgainst); + invokeAgainst = invokeResult.get(fieldStack.get(++i)); + cls = invokeAgainst.getClass(); + } else { + invokeAgainst = m.invoke(invokeAgainst); + } + } + + Field[] fields = cls.getDeclaredFields(); + + return Arrays.stream(fields).map(Field::getName).collect(Collectors.toList()); + } +} diff --git a/langserver-core/src/main/java/org/swagger/langserver/completion/FieldIdentifier.java b/langserver-core/src/main/java/org/swagger/langserver/completion/FieldIdentifier.java new file mode 100644 index 0000000..bea5506 --- /dev/null +++ b/langserver-core/src/main/java/org/swagger/langserver/completion/FieldIdentifier.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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.swagger.langserver.completion; + +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; + +/** + * Go through the yml config to identify the field stack where the cursor belongs. + */ +class FieldIdentifier { + + private Deque fieldStack = new ArrayDeque<>(); + + private boolean terminateVisit = false; + + void calculateFieldStack(List tuples, int cursorLine, int cursorCol, int parentIndentation) { + for (NodeTuple tuple : tuples) { + String key = ((ScalarNode) tuple.getKeyNode()).getValue(); + int line = tuple.getKeyNode().getStartMark().getLine(); + int col = tuple.getKeyNode().getStartMark().getColumn(); + + if (cursorLine <= line) { + if (cursorCol <= parentIndentation) { + this.fieldStack.pop(); + } + this.terminateVisit = true; + break; + } else if (tuple.getValueNode() instanceof MappingNode && cursorCol > col) { + this.fieldStack.push(key); + this.calculateFieldStack(((MappingNode) tuple.getValueNode()).getValue(), cursorLine, cursorCol, col); + } + + if (!terminateVisit && tuples.indexOf(tuple) == tuples.size() - 1 && !fieldStack.isEmpty()) { + this.fieldStack.pop(); + } + } + } + + Deque getFieldStack() { + return fieldStack; + } +} diff --git a/pom.xml b/pom.xml index 9d56d31..4c04e5b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,34 @@ - - + + + 4.0.0 - org.swagger - swagger-langserver - 1.0-SNAPSHOT + org.swagger.ls + swagger-language-server + pom + 0.0.1-SNAPSHOT + + langserver-core + stdio-launcher + @@ -39,5 +63,7 @@ 0.4.1 + 6.14.3 + 1.7.22 \ No newline at end of file diff --git a/stdio-launcher/pom.xml b/stdio-launcher/pom.xml new file mode 100644 index 0000000..a8743f2 --- /dev/null +++ b/stdio-launcher/pom.xml @@ -0,0 +1,123 @@ + + + + + swagger-language-server + org.swagger.ls + 0.0.1-SNAPSHOT + + 4.0.0 + + ls-launcher + + + + org.swagger.ls + langserver-core + 0.0.1-SNAPSHOT + + + org.testng + testng + ${testng.version} + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-nop + ${slf4j.version} + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + StdioLauncher + + + ${project.artifactId} + + + + package + + shade + + + + + + maven-resources-plugin + 3.1.0 + + + copy-files-on-build + package + + copy-resources + + + ${basedir}/../client/launcher/ + + + ${basedir}/target/ + ${project.artifactId}.jar + true + + + + + + + + + \ No newline at end of file diff --git a/stdio-launcher/src/main/java/StdioLauncher.java b/stdio-launcher/src/main/java/StdioLauncher.java new file mode 100644 index 0000000..d25004b --- /dev/null +++ b/stdio-launcher/src/main/java/StdioLauncher.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018, Nadeeshaan Gunasinghe + * + * 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. + */ + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageClient; +import org.swagger.langserver.SwaggerLanguageServer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * STDIO Launcher to launch the Language Server. + */ +public class StdioLauncher { + public static void main(String[] args) throws InterruptedException, ExecutionException { + LogManager.getLogManager().reset(); + Logger globalLogger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + globalLogger.setLevel(Level.OFF); + + startServer(System.in, System.out); + } + + private static void startServer(InputStream in, OutputStream out) + throws InterruptedException, ExecutionException { + SwaggerLanguageServer server = new SwaggerLanguageServer(); + Launcher l = LSPLauncher.createServerLauncher(server, in, out); + LanguageClient client = l.getRemoteProxy(); + server.connect(client); + Future startListening = l.startListening(); + startListening.get(); + } +}