diff --git a/vscode-extensions/vscode-application-yaml/.classpath b/vscode-extensions/vscode-application-yaml/.classpath new file mode 100644 index 000000000..d46faf10e --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.classpath @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vscode-extensions/vscode-application-yaml/.gitignore b/vscode-extensions/vscode-application-yaml/.gitignore new file mode 100644 index 000000000..2932a75be --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.gitignore @@ -0,0 +1,11 @@ +out +node_modules +target +*.log +*.log.* +.idea +*.iml +dependency-reduced-pom.xml +classpath.txt +*.vsix +repo \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/.project b/vscode-extensions/vscode-application-yaml/.project new file mode 100644 index 000000000..e81ccc46f --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.project @@ -0,0 +1,23 @@ + + + vscode-application-yaml + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.jdt.core.prefs b/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..714351aec --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.m2e.core.prefs b/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..f897a7f1c --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/vscode-extensions/vscode-application-yaml/.vscode/launch.json b/vscode-extensions/vscode-application-yaml/.vscode/launch.json new file mode 100644 index 000000000..52ebf9f9e --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.vscode/launch.json @@ -0,0 +1,28 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/lib", + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/test", + "preLaunchTask": "npm" + } + ] +} diff --git a/vscode-extensions/vscode-application-yaml/.vscode/settings.json b/vscode-extensions/vscode-application-yaml/.vscode/settings.json new file mode 100644 index 000000000..c5592bee9 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.vscode/settings.json @@ -0,0 +1,12 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": true, // set this to true to hide the "out" folder with the compiled JS files + "node_modules": false, + "target": true + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/.vscode/tasks.json b/vscode-extensions/vscode-application-yaml/.vscode/tasks.json new file mode 100644 index 000000000..1cbb9fd45 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.vscode/tasks.json @@ -0,0 +1,30 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +// A task runner that calls a custom npm script that compiles the extension. +{ + "version": "0.1.0", + + // we want to run npm + "command": "npm", + + // the command is a shell script + "isShellCommand": true, + + // show the output window only if unrecognized errors occur. + "showOutput": "silent", + + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile"], //, "--loglevel", "silent"], + + // The tsc compiler is started in watching mode + "isWatching": true, + + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/.vscodeignore b/vscode-extensions/vscode-application-yaml/.vscodeignore new file mode 100644 index 000000000..71d147879 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/.vscodeignore @@ -0,0 +1,30 @@ +# IDE configs +.vscode/** +.idea/** +*.iml +javaconfig.json +classpath.txt +tsconfig.json +tsd.json +*.xml + +# Logs +*.log* + +# Sources +typings/** +src/** +test/** +lib/** +!lib/javaconfig.schema.json +repo/** +scripts/** + +# Compiler output +out/test/** +target/** +!target/fat-jar.jar + +# Extensions +.gitignore +**/*.map diff --git a/vscode-extensions/vscode-application-yaml/README.md b/vscode-extensions/vscode-application-yaml/README.md new file mode 100644 index 000000000..cd4e5c9e7 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/README.md @@ -0,0 +1,53 @@ +# VS Code Language Server Example in Java + +The repo contains the end-result of an 'exercise' to implement +a simple VS Code Language Server in Java. + +The starting point is a clone of this repo: + +https://github.com/georgewfraser/vscode-javac + +It provides Java support using the javac API. + +As such that repo contains all the pieces necessary +to create a working Language Server. In this +exercise we stripped it down to the bare minimum +to make a very basic language server similar to +the one built in the +[VScode tutorial on building a language server](https://code.visualstudio.com/docs/extensions/example-language-server). + +# Running + +The extension implemented in this example consists out of two pieces: + + - client: a typescript js app that launches and connects to the language server. + - server: server app, implemented in Java. + +First build the server: + + mvn clean package + +The server will be produced in `out/fat-jar.jar`. + +Then build the client: + + npm clean install + +Now you can open the client-app in vscode. From the root of this project. + + code . + +To launch the language server in a vscode runtime, press F5. + +# Debugging + +To debug the language server, open `lib/Main.ts` and edit to set the +`DEBUG` constant to `true`. When you laucnh the app next by pressing +`F5` it will launch with debug options being passed to the JVM. + +You can then connect a 'Remote Java' Eclipse debugger on port 8000. + +Note that in debug mode we launch not from the 'fatjar' produced by the +maven build, but instead use the classes from 'target/classes' directory. +This allows you to edit the server code in Eclipse and relaunch the +client from vscode without rebuilding the fatjar. diff --git a/vscode-extensions/vscode-application-yaml/icon.png b/vscode-extensions/vscode-application-yaml/icon.png new file mode 100644 index 000000000..818a27929 Binary files /dev/null and b/vscode-extensions/vscode-application-yaml/icon.png differ diff --git a/vscode-extensions/vscode-application-yaml/lib/.gitignore b/vscode-extensions/vscode-application-yaml/lib/.gitignore new file mode 100644 index 000000000..a6c7c2852 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/lib/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/vscode-extensions/vscode-application-yaml/lib/Main.ts b/vscode-extensions/vscode-application-yaml/lib/Main.ts new file mode 100644 index 000000000..9f447ca0d --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/lib/Main.ts @@ -0,0 +1,170 @@ +'use strict'; +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below + +import * as VSCode from 'vscode'; +import * as Path from 'path'; +import * as FS from 'fs'; +import * as PortFinder from 'portfinder'; +import * as Net from 'net'; +import * as ChildProcess from 'child_process'; +import {LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, StreamInfo} from 'vscode-languageclient'; + +PortFinder.basePort = 55282; + +var DEBUG = false; +const DEBUG_ARG = '-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n'; + //If DEBUG is falsy then + // we launch from the 'fat jar' (which has to be built by running mvn package) + //if DEBUG is truthy then + // - we launch the Java project directly from the classes folder produced by Eclipse JDT compiler + // - we add DEBUG_ARG to the launch so that remote debugger can attach on port 8000 + +function getClasspath(context: VSCode.ExtensionContext):string { + if (DEBUG) { + try { + let projectDir = context.extensionPath; + let classpathFile = Path.resolve(projectDir, "classpath.txt"); + //TODO: async read? + let classpath = FS.readFileSync(classpathFile, 'utf8'); + classpath = Path.resolve(projectDir, 'target/classes') + ':' + classpath; + return classpath; + } catch (e) { + //Expected if you classpath.txt file isn't packaged. So this means we are running in packaged mode. + VSCode.window.showInformationMessage('classpath file not found, so disabling DEBUG mode '+e); + //Nasty! Be careful, this assumes 'getClasspath' is called before computing debug args. + DEBUG = false; + } + } + return Path.resolve(context.extensionPath, "out", "fat-jar.jar"); +} + +/** Called when extension is activated */ +export function activate(context: VSCode.ExtensionContext) { + let javaExecutablePath = findJavaExecutable('java'); + + if (javaExecutablePath == null) { + VSCode.window.showErrorMessage("Couldn't locate java in $JAVA_HOME or $PATH"); + return; + } + + isJava8(javaExecutablePath).then(eight => { + if (!eight) { + VSCode.window.showErrorMessage('Java language support requires Java 8 (using ' + javaExecutablePath + ')'); + return; + } + + // Options to control the language client + let clientOptions: LanguageClientOptions = { + // Register the server for java documents + documentSelector: ['yaml'], + synchronize: { + // Synchronize the setting section to the server: + configurationSection: 'languageServerExample', + // Notify the server about file changes to 'javaconfig.json' files contain in the workspace + fileEvents: [ + //What's this for? Don't think it does anything useful for this example: + VSCode.workspace.createFileSystemWatcher('**/.clientrc') + ] + } + } + + function createServer(): Promise { + return new Promise((resolve, reject) => { + PortFinder.getPort((err, port) => { + Net.createServer(socket => { + console.log('Child process connected on port ' + port); + + resolve({ + reader: socket, + writer: socket + }); + }).listen(port, () => { + let options = { + cwd: VSCode.workspace.rootPath + }; + let child: ChildProcess.ChildProcess; + let classpath = getClasspath(context); + let args = [ + '-Dserver.port=' + port, + '-cp', classpath, + 'com.github.kdvolder.lsapi.example.Main' + ]; + if (DEBUG) { + args.unshift(DEBUG_ARG); + } + console.log(javaExecutablePath + ' ' + args.join(' ')); + + // Start the child java process + child = ChildProcess.execFile(javaExecutablePath, args, options); + child.stdout.on('data', (data) => { + console.log(data); + }); + child.stderr.on('data', (data) => { + console.error(data); + }) + }); + }); + }); + } + + // Create the language client and start the client. + let client = new LanguageClient('lsapi-example', 'Language Server Example', + createServer, clientOptions); + let disposable = client.start(); + + // Push the disposable to the context's subscriptions so that the + // client can be deactivated on extension deactivation + context.subscriptions.push(disposable); + }); +} + +function isJava8(javaExecutablePath: string): Promise { + return new Promise((resolve, reject) => { + let result = ChildProcess.execFile(javaExecutablePath, ['-version'], { }, (error, stdout, stderr) => { + let eight = stderr.indexOf('1.8') >= 0; + + resolve(eight); + }); + }); +} + +function findJavaExecutable(binname: string) { + binname = correctBinname(binname); + + // First search each JAVA_HOME bin folder + if (process.env['JAVA_HOME']) { + let workspaces = process.env['JAVA_HOME'].split(Path.delimiter); + for (let i = 0; i < workspaces.length; i++) { + let binpath = Path.join(workspaces[i], 'bin', binname); + if (FS.existsSync(binpath)) { + return binpath; + } + } + } + + // Then search PATH parts + if (process.env['PATH']) { + let pathparts = process.env['PATH'].split(Path.delimiter); + for (let i = 0; i < pathparts.length; i++) { + let binpath = Path.join(pathparts[i], binname); + if (FS.existsSync(binpath)) { + return binpath; + } + } + } + + // Else return the binary name directly (this will likely always fail downstream) + return null; +} + +function correctBinname(binname: string) { + if (process.platform === 'win32') + return binname + '.exe'; + else + return binname; +} + +// this method is called when your extension is deactivated +export function deactivate() { +} diff --git a/vscode-extensions/vscode-application-yaml/package.json b/vscode-extensions/vscode-application-yaml/package.json new file mode 100644 index 000000000..407f4ddf8 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/package.json @@ -0,0 +1,59 @@ +{ + "name": "vscode-application-yml", + "displayName": "Spring Boot Application YML Support", + "description": "Provides validation and content assist for spring-boot application.yml file", + "icon": "icon.png", + "version": "0.0.1", + "publisher": "kdvolder", + "repository": { + "type": "git", + "url": "https://github.com/kdvolder/vscode-lsapi-example.git" + }, + "license": "EPL", + "engines": { + "vscode": "^0.10.10" + }, + "categories": [ + "Languages", + "Linters" + ], + "keywords": [ + "yaml", "springboot" + ], + "activationEvents": [ + "onLanguage:yaml" + ], + "main": "./out/lib/Main", + "files": [ + "target/fat-jar.jar" + ], + "contributes": { + "configuration": { + "type": "object", + "title": "Example configuration", + "properties": { + "languageServerExample.maxNumberOfProblems": { + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server." + } + } + } + }, + "preview": "true", + "scripts": { + "vscode:prepublish": "node ./node_modules/vscode/bin/compile", + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "mocha out/test" + }, + "dependencies": { + "portfinder": "^0.4.0", + "vscode-languageclient": "^2.2.1" + }, + "devDependencies": { + "typescript": "^1.8.5", + "vscode": "^0.11.0", + "mocha": "^2.4.5" + } +} diff --git a/vscode-extensions/vscode-application-yaml/package.sh b/vscode-extensions/vscode-application-yaml/package.sh new file mode 100755 index 000000000..fe8487162 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/package.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +## run this script to package up this extension using vsce tool. +## This assumes you have vsce tool installed. +## You can install it via npm + +set -e # fail at the first sign of trouble + +mvn clean package ## build language server fat jar +npm install +vsce package + + diff --git a/vscode-extensions/vscode-application-yaml/pom.xml b/vscode-extensions/vscode-application-yaml/pom.xml new file mode 100644 index 000000000..94891dee0 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/pom.xml @@ -0,0 +1,166 @@ + + + 4.0.0 + com.github.kdvolder + vscode-application-yaml + jar + 0.0.1-SNAPSHOT + + + + jars-repository + Local repository for JAR files + file://${basedir}/repo + + + + + oss-sonatype + oss-sonatype + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + + + distribution-repository + Temporary Staging Repository + file://${basedir}/dist + + + + 2.5.0 + 2.10 + + + + + io.typefox.lsapi + io.typefox.lsapi + 0.2.0-SNAPSHOT + + + io.typefox.lsapi + io.typefox.lsapi.services + 0.2.0-SNAPSHOT + + + io.typefox.lsapi + io.typefox.lsapi.annotations + 0.2.0-SNAPSHOT + + + + com.fasterxml.jackson.core + jackson-core + ${jackson-2-version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-2-version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-2-version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-2-version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson-2-version} + + + + org.yaml + snakeyaml + 1.17 + + + + com.google.guava + guava + 18.0 + + + + junit + junit + 4.11 + test + + + org.assertj + assertj-core + 3.5.2 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.9 + + + build-classpath + generate-sources + + build-classpath + + + + + classpath.txt + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.1 + + ${project.basedir}/out/fat-jar.jar + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + + diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/ErrorCodes.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/ErrorCodes.java new file mode 100644 index 000000000..2d7f82178 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/ErrorCodes.java @@ -0,0 +1,5 @@ +package com.github.kdvolder.lsapi.example; + +public class ErrorCodes { + public static final String YAML_SYNTAX_ERROR = "YAML_SYNTAX_ERROR"; +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/Main.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/Main.java new file mode 100644 index 000000000..5f76a76a4 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/Main.java @@ -0,0 +1,99 @@ +package com.github.kdvolder.lsapi.example; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.github.kdvolder.lsapi.util.LoggingFormat; + +import io.typefox.lsapi.services.json.LoggingJsonAdapter; + +public class Main { + private static final Logger LOG = Logger.getLogger("main"); + + public static void main(String[] args) throws IOException { + LOG.info("Starting LS"); + try { + LoggingFormat.startLogging(); + + Connection connection = connectToNode(); + + run(connection); + } catch (Throwable t) { + LOG.log(Level.SEVERE, t.getMessage(), t); + + System.exit(1); + } + } + + private static Connection connectToNode() throws IOException { + String port = System.getProperty("server.port"); + + if (port != null) { + Socket socket = new Socket("localhost", Integer.parseInt(port)); + + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + + OutputStream intercept = new OutputStream() { + + @Override + public void write(int b) throws IOException { + out.write(b); + } + }; + + LOG.info("Connected to parent using socket on port " + port); + + return new Connection(in, intercept); + } + else { + InputStream in = System.in; + PrintStream out = System.out; + + LOG.info("Connected to parent using stdio"); + + return new Connection(in, out); + } + } + + private static class Connection { + final InputStream in; + final OutputStream out; + + private Connection(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + } + } + + /** + * Listen for requests from the parent node process. + * Send replies asynchronously. + * When the request stream is closed, wait for 5s for all outstanding responses to compute, then return. + */ + public static void run(Connection connection) { + YamlLanguageServer server = new YamlLanguageServer(); + LoggingJsonAdapter jsonServer = new LoggingJsonAdapter(server); + jsonServer.setMessageLog(new PrintWriter(System.out)); + + jsonServer.connect(connection.in, connection.out); + jsonServer.getProtocol().addErrorListener((message, err) -> { + LOG.log(Level.SEVERE, message, err); + + server.onError(message, err); + }); + + try { + jsonServer.join(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/YamlLanguageServer.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/YamlLanguageServer.java new file mode 100644 index 000000000..2fb2cdbaa --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/example/YamlLanguageServer.java @@ -0,0 +1,168 @@ +package com.github.kdvolder.lsapi.example; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.MarkedYAMLException; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.Node; + +import com.github.kdvolder.lsapi.util.Futures; +import com.github.kdvolder.lsapi.util.SimpleLanguageServer; +import com.github.kdvolder.lsapi.util.SimpleTextDocumentService; +import com.github.kdvolder.lsapi.util.TextDocument; +import com.google.common.collect.ImmutableList; + +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionItemImpl; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.CompletionListImpl; +import io.typefox.lsapi.CompletionOptionsImpl; +import io.typefox.lsapi.Diagnostic; +import io.typefox.lsapi.DiagnosticImpl; +import io.typefox.lsapi.PositionImpl; +import io.typefox.lsapi.RangeImpl; +import io.typefox.lsapi.ServerCapabilities; +import io.typefox.lsapi.ServerCapabilitiesImpl; + +public class YamlLanguageServer extends SimpleLanguageServer { + + private Yaml yaml = new Yaml(); + + public YamlLanguageServer() { + SimpleTextDocumentService documents = getTextDocumentService(); +// SimpleWorkspaceService workspace = getWorkspaceService(); + documents.onDidChangeContent(params -> { + System.out.println("Document changed: "+params); + TextDocument doc = params.getDocument(); + validateDocument(documents, doc); + }); + +// workspace.onDidChangeConfiguraton(settings -> { +// System.out.println("Config changed: "+params); +// Integer val = settings.getInt("languageServerExample", "maxNumberOfProblems"); +// if (val!=null) { +// maxProblems = ((Number) val).intValue(); +// for (TextDocument doc : documents.getAll()) { +// validateDocument(documents, doc); +// } +// } +// }); + + documents.onCompletion(params -> { + CompletableFuture promise = new CompletableFuture<>(); + CompletionListImpl completions = new CompletionListImpl(); + completions.setIncomplete(false); + List items = new ArrayList<>(); + { +// { +// label: 'TypeScript', +// kind: CompletionItemKind.Text, +// data: 1 +// }, + CompletionItemImpl item = new CompletionItemImpl(); + item.setLabel("TypeScript"); + item.setKind(CompletionItem.KIND_TEXT); + item.setData(1); + items.add(item); + } + + { +// { +// label: 'JavaScript', +// kind: CompletionItemKind.Text, +// data: 2 +// } + CompletionItemImpl item = new CompletionItemImpl(); + item.setLabel("JavaScript"); + item.setKind(CompletionItem.KIND_TEXT); + item.setData(2); + items.add(item); + } + completions.setItems(items); + + promise.complete(completions); + return promise; + }); + + documents.onCompletionResolve((_item) -> { + CompletionItemImpl item = (CompletionItemImpl) _item; + Object data = item.getData(); + if (Integer.valueOf(1).equals(data)) { + item.setDetail("TypeScript details"); + item.setDocumentation("TypeScript docs"); + } else { + item.setDetail("JavaScript details"); + item.setDocumentation("JavaScript docs"); + } + return Futures.of((CompletionItem)item); + }); + } + + private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) { + List diagnostics = reconcile(documents, doc); + documents.publishDiagnostics(doc, diagnostics); + } + + protected List reconcile(SimpleTextDocumentService documents, TextDocument doc) { + try { + Iterator asts = yaml.composeAll(new StringReader(doc.getText())).iterator(); + while (asts.hasNext()) { + asts.next(); + } + return ImmutableList.of(); + } catch (YAMLException e) { + return ImmutableList.of(parseError(e)); + } + } + + private DiagnosticImpl parseError(YAMLException e) { + DiagnosticImpl d = new DiagnosticImpl(); + d.setMessage(getMessage(e)); + d.setRange(getRange(e)); + d.setSeverity(Diagnostic.SEVERITY_ERROR); + d.setCode(ErrorCodes.YAML_SYNTAX_ERROR); + d.setSource("yaml"); + return d; + } + + private String getMessage(YAMLException e) { + if (e instanceof MarkedYAMLException) { + return ((MarkedYAMLException) e).getProblem(); + } + return e.getMessage(); + } + + private RangeImpl getRange(YAMLException _e) { + if (_e instanceof MarkedYAMLException) { + MarkedYAMLException e = (MarkedYAMLException) _e; + + PositionImpl start = new PositionImpl(); + start.setLine(e.getProblemMark().getLine()); + start.setCharacter(e.getProblemMark().getColumn()); + + RangeImpl rng = new RangeImpl(); + rng.setStart(start); + rng.setEnd(start); + return rng; + } + return null; + } + + @Override + protected ServerCapabilitiesImpl getServerCapabilities() { + ServerCapabilitiesImpl c = new ServerCapabilitiesImpl(); + + c.setTextDocumentSync(ServerCapabilities.SYNC_FULL); + + CompletionOptionsImpl completionProvider = new CompletionOptionsImpl(); + completionProvider.setResolveProvider(true); + c.setCompletionProvider(completionProvider); + + return c; + } +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Assert.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Assert.java new file mode 100644 index 000000000..38b3ee052 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Assert.java @@ -0,0 +1,11 @@ +package com.github.kdvolder.lsapi.util; + +public class Assert { + + public static void isNull(String msg, Object obj) { + if (obj!=null) { + throw new IllegalStateException(msg); + } + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionHandler.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionHandler.java new file mode 100644 index 000000000..eb8069d39 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionHandler.java @@ -0,0 +1,11 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.concurrent.CompletableFuture; + +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.TextDocumentPositionParams; + +@FunctionalInterface +public interface CompletionHandler { + CompletableFuture handle(TextDocumentPositionParams params); +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionResolveHandler.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionResolveHandler.java new file mode 100644 index 000000000..e01ffd140 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/CompletionResolveHandler.java @@ -0,0 +1,10 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.concurrent.CompletableFuture; + +import io.typefox.lsapi.CompletionItem; + +@FunctionalInterface +public interface CompletionResolveHandler { + CompletableFuture handle(CompletionItem unresolved); +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Futures.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Futures.java new file mode 100644 index 000000000..bb4e01bf9 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Futures.java @@ -0,0 +1,13 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.concurrent.CompletableFuture; + +public class Futures { + + public static CompletableFuture of(T value) { + CompletableFuture f = new CompletableFuture(); + f.complete(value); + return f; + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/JSON.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/JSON.java new file mode 100644 index 000000000..2177de2e8 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/JSON.java @@ -0,0 +1,51 @@ +package com.github.kdvolder.lsapi.util; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; + +public class JSON { + + public static final ObjectMapper JSON = new ObjectMapper().registerModule(new Jdk8Module()) + .registerModule(new JSR310Module()) + .registerModule(pathAsJson()) + .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + + private static SimpleModule pathAsJson() { + SimpleModule m = new SimpleModule(); + + m.addSerializer(Path.class, new JsonSerializer() { + @Override + public void serialize(Path path, + JsonGenerator gen, + SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + gen.writeString(path.toString()); + } + }); + + m.addDeserializer(Path.class, new JsonDeserializer() { + @Override + public Path deserialize(JsonParser parse, + DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + return Paths.get(parse.getText()); + } + }); + + return m; + } + + + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ListenerList.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ListenerList.java new file mode 100644 index 000000000..e35f1de6f --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ListenerList.java @@ -0,0 +1,21 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class ListenerList { + + private List> listeners = new ArrayList<>(); + + public synchronized void fire(T evt) { + for (Consumer l : listeners) { + l.accept(evt); + } + } + + public void add(Consumer l) { + listeners.add(l); + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/LoggingFormat.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/LoggingFormat.java new file mode 100644 index 000000000..4237ce473 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/LoggingFormat.java @@ -0,0 +1,67 @@ +package com.github.kdvolder.lsapi.util; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.Instant; +import java.util.StringJoiner; +import java.util.logging.*; + +public class LoggingFormat extends Formatter { + public static ThreadLocal request = new ThreadLocal<>(); + + @Override + public String format(LogRecord record) { + StringJoiner joiner = new StringJoiner("\t"); + + joiner.add(record.getLevel().getName()); + joiner.add(requestAsString()); + joiner.add(Thread.currentThread().getName()); + joiner.add(Instant.ofEpochMilli(record.getMillis()).toString()); +// joiner.add(record.getLoggerName()); + joiner.add(record.getSourceClassName() + "#" + record.getSourceMethodName()); + joiner.add(record.getMessage()); + + String result = joiner.toString() + "\n"; + + Throwable thrown = record.getThrown(); + + if (thrown != null) { + StringWriter stackTrace = new StringWriter(); + PrintWriter print = new PrintWriter(stackTrace); + + thrown.printStackTrace(print); + print.flush(); + + result = result + stackTrace + "\n"; + } + + return result; + } + + private CharSequence requestAsString() { + Integer i = request.get(); + + if (i == null) + return "?"; + else + return Integer.toString(i); + } + + private static boolean started = false; + + public static void startLogging() throws IOException { + if (!started) { + started = true; + + Logger root = Logger.getLogger(""); + + FileHandler file = new FileHandler("javac-services.%g.log", 100_000, 1, false); + + root.addHandler(file); + + for (Handler h : root.getHandlers()) + h.setFormatter(new LoggingFormat()); + } + } +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Settings.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Settings.java new file mode 100644 index 000000000..7812ca45a --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/Settings.java @@ -0,0 +1,43 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.Map; + +/** + * Convenience wrapper around a 'settings' object. Provides some useful accessor methods to + * retrieve properties from the settings object. + */ +public class Settings { + + private Object settings; + + public Settings(Object settings) { + this.settings = settings; + } + + public Integer getInt(String... names) { + Object val = getProperty(names); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return null; + } + + public Object getProperty(String... names) { + return getProperty(settings, names, 0); + } + + @SuppressWarnings("rawtypes") + private static Object getProperty(Object settings, String[] names, int i) { + if (i >= names.length) { + return settings; + } else if (settings instanceof Map) { + Object sub = ((Map)settings).get(names[i]); + return getProperty(sub, names, i+1); + } else { + return null; + } + } + + + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ShowMessageException.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ShowMessageException.java new file mode 100644 index 000000000..6eadd37c7 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/ShowMessageException.java @@ -0,0 +1,33 @@ +package com.github.kdvolder.lsapi.util; + +import io.typefox.lsapi.MessageParams; +import io.typefox.lsapi.MessageParamsImpl; + +public class ShowMessageException extends RuntimeException { + + private static final long serialVersionUID = 1L; + public final MessageParams message; + + public ShowMessageException(MessageParams message, Exception cause) { + super(message.getMessage(), cause); + + this.message = message; + } + + public static ShowMessageException error(String message, Exception cause) { + return create(MessageParams.TYPE_ERROR, message, cause); + } + + public static ShowMessageException warning(String message, Exception cause) { + return create(MessageParams.TYPE_WARNING, message, cause); + } + + private static ShowMessageException create(int type, String message, Exception cause) { + MessageParamsImpl m = new MessageParamsImpl(); + + m.setMessage(message); + m.setType(type); + + return new ShowMessageException(m, cause); + } +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleLanguageServer.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleLanguageServer.java new file mode 100644 index 000000000..9b24a6c9c --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleLanguageServer.java @@ -0,0 +1,131 @@ +package com.github.kdvolder.lsapi.util; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.typefox.lsapi.InitializeParams; +import io.typefox.lsapi.InitializeResult; +import io.typefox.lsapi.InitializeResultImpl; +import io.typefox.lsapi.MessageParams; +import io.typefox.lsapi.MessageParamsImpl; +import io.typefox.lsapi.ServerCapabilitiesImpl; +import io.typefox.lsapi.ShowMessageRequestParams; +import io.typefox.lsapi.services.LanguageServer; +import io.typefox.lsapi.services.WindowService; + +/** + * Abstract base class to implement LanguageServer. Bits and pieces copied from + * the 'JavaLanguageServer' example which seem generally useful / reusable end up in + * here so we can try to keep the subclass itself more 'clutter free' and focus on + * what its really doing and not the 'wiring and plumbing'. + */ +public abstract class SimpleLanguageServer implements LanguageServer { + + private static final Logger LOG = Logger.getLogger(SimpleLanguageServer.class.getName()); + + private Consumer showMessage = m -> {}; + + private Path workspaceRoot; + + private SimpleTextDocumentService tds; + + private SimpleWorkspaceService workspace; + + @Override + public CompletableFuture initialize(InitializeParams params) { + LOG.info("Initializing"); + String rootPath = params.getRootPath(); + if (rootPath==null) { + LOG.warning("workspaceRoot NOT SET"); + } else { + this.workspaceRoot= Paths.get(rootPath).toAbsolutePath().normalize(); + LOG.info("workspaceRoot = "+workspaceRoot); + } + + InitializeResultImpl result = new InitializeResultImpl(); + + ServerCapabilitiesImpl cap = getServerCapabilities(); + result.setCapabilities(cap); + + return CompletableFuture.completedFuture(result); + } + + @Override + public WindowService getWindowService() { + return new WindowService() { + @Override + public void onShowMessage(Consumer callback) { + showMessage = callback; + } + + @Override + public void onShowMessageRequest(Consumer callback) { + + } + + @Override + public void onLogMessage(Consumer callback) { + + } + }; + } + + public void onError(String message, Throwable error) { + if (error instanceof ShowMessageException) + showMessage.accept(((ShowMessageException) error).message); + else { + LOG.log(Level.SEVERE, message, error); + + MessageParamsImpl m = new MessageParamsImpl(); + + m.setMessage(message); + m.setType(MessageParams.TYPE_ERROR); + + showMessage.accept(m); + } + } + + protected abstract ServerCapabilitiesImpl getServerCapabilities(); + + @Override + public void shutdown() { + } + + @Override + public void exit() { + } + + + public Path getWorkspaceRoot() { + return workspaceRoot; + } + + @Override + public synchronized SimpleTextDocumentService getTextDocumentService() { + if (tds==null) { + tds = createTextDocumentService(); + } + return tds; + } + + protected SimpleTextDocumentService createTextDocumentService() { + return new SimpleTextDocumentService(); + } + + public SimpleWorkspaceService createWorkspaceService() { + return new SimpleWorkspaceService(); + } + + @Override + public synchronized SimpleWorkspaceService getWorkspaceService() { + if (workspace==null) { + workspace = createWorkspaceService(); + } + return workspace; + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleTextDocumentService.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleTextDocumentService.java new file mode 100644 index 000000000..6ca7d849f --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleTextDocumentService.java @@ -0,0 +1,271 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import io.typefox.lsapi.CodeActionParams; +import io.typefox.lsapi.CodeLens; +import io.typefox.lsapi.CodeLensParams; +import io.typefox.lsapi.Command; +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.DiagnosticImpl; +import io.typefox.lsapi.DidChangeTextDocumentParams; +import io.typefox.lsapi.DidCloseTextDocumentParams; +import io.typefox.lsapi.DidOpenTextDocumentParams; +import io.typefox.lsapi.DidSaveTextDocumentParams; +import io.typefox.lsapi.DocumentFormattingParams; +import io.typefox.lsapi.DocumentHighlight; +import io.typefox.lsapi.DocumentOnTypeFormattingParams; +import io.typefox.lsapi.DocumentRangeFormattingParams; +import io.typefox.lsapi.DocumentSymbolParams; +import io.typefox.lsapi.Hover; +import io.typefox.lsapi.Location; +import io.typefox.lsapi.PublishDiagnosticsParams; +import io.typefox.lsapi.PublishDiagnosticsParamsImpl; +import io.typefox.lsapi.Range; +import io.typefox.lsapi.ReferenceParams; +import io.typefox.lsapi.RenameParams; +import io.typefox.lsapi.SignatureHelp; +import io.typefox.lsapi.SymbolInformation; +import io.typefox.lsapi.TextDocumentContentChangeEvent; +import io.typefox.lsapi.TextDocumentPositionParams; +import io.typefox.lsapi.TextEdit; +import io.typefox.lsapi.VersionedTextDocumentIdentifier; +import io.typefox.lsapi.WorkspaceEdit; +import io.typefox.lsapi.services.TextDocumentService; + +public class SimpleTextDocumentService implements TextDocumentService { + + private static final Logger LOG = Logger.getLogger(SimpleTextDocumentService.class.getName()); + + private Consumer publishDiagnostics = (p) -> {}; + + private Map documents = new HashMap<>(); + private ListenerList documentChangeListeners = new ListenerList<>(); + private CompletionHandler completionHandler = null; + private CompletionResolveHandler completionResolveHandler = null; + + public synchronized void onCompletion(CompletionHandler h) { + Assert.isNull("A completion handler is already set, multiple handlers not supported yet", completionHandler); + this.completionHandler = h; + } + + public synchronized void onCompletionResolve(CompletionResolveHandler h) { + Assert.isNull("A completionResolveHandler handler is already set, multiple handlers not supported yet", completionResolveHandler); + this.completionResolveHandler = h; + } + + /** + * Gets all documents this service is tracking, generally these are the documents that have been opened / changed, + * and not yet closed. + */ + public synchronized Collection getAll() { + return new ArrayList<>(documents.values()); + } + + + @Override + public final void didChange(DidChangeTextDocumentParams params) { + VersionedTextDocumentIdentifier docId = params.getTextDocument(); + String url = docId.getUri(); + if (url!=null) { + LOG.info("Document changed: "+url); + TextDocument doc = getOrCreateDocument(url); + for (TextDocumentContentChangeEvent change : params.getContentChanges()) { + doc.apply(change); + didChangeContent(doc, change); + } + } + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + //LOG.info("didOpen: "+params); + //Example message: + //{ + // "jsonrpc":"2.0", + // "method":"textDocument/didOpen", + // "params":{ + // "textDocument":{ + // "uri":"file:///home/kdvolder/tmp/hello-java/hello.txt", + // "languageId":"plaintext", + // "version":1, + // "text":"This is some text ya-all o\nsss typescript\n" + // } + // } + //} + String url = params.getTextDocument().getUri(); + if (url!=null) { + String text = params.getTextDocument().getText(); + TextDocument doc = getOrCreateDocument(url); + doc.setText(text); + TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent() { + @Override + public Range getRange() { + return null; + } + + @Override + public Integer getRangeLength() { + return null; + } + + @Override + public String getText() { + return text; + } + }; + TextDocumentContentChange evt = new TextDocumentContentChange(doc, change); + documentChangeListeners.fire(evt); + } + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + String url = params.getTextDocument().getUri(); + if (url!=null) { + documents.remove(url); + } + } + + + void didChangeContent(TextDocument doc, TextDocumentContentChangeEvent change) { + documentChangeListeners.fire(new TextDocumentContentChange(doc, change)); + } + + public void onDidChangeContent(Consumer l) { + documentChangeListeners.add(l); + } + + private synchronized TextDocument getOrCreateDocument(String url) { + TextDocument doc = documents.get(url); + if (doc==null) { + documents.put(url, doc = new TextDocument(url)); + } + return doc; + } + + @Override + public CompletableFuture completion(TextDocumentPositionParams position) { + CompletionHandler h = completionHandler; + if (h!=null) { + return completionHandler.handle(position); + } + return null; //TODO: does caller handle nulls? Or do we need to provide something that create a empty completion list? + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem unresolved) { + CompletionResolveHandler h = completionResolveHandler; + if (h!=null) { + return h.handle(unresolved); + } + return null; + } + + @Override + public CompletableFuture hover(TextDocumentPositionParams position) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture signatureHelp(TextDocumentPositionParams position) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> definition(TextDocumentPositionParams position) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> references(ReferenceParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture documentHighlight(TextDocumentPositionParams position) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> documentSymbol(DocumentSymbolParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> codeAction(CodeActionParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> codeLens(CodeLensParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture resolveCodeLens(CodeLens unresolved) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public CompletableFuture rename(RenameParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + // TODO Auto-generated method stub + + } + + @Override + public synchronized void onPublishDiagnostics(Consumer callback) { + publishDiagnostics = publishDiagnostics.andThen(callback); + } + + public void publishDiagnostics(TextDocument doc, List diagnostics) { + if (diagnostics!=null && !diagnostics.isEmpty()) { + PublishDiagnosticsParamsImpl params = new PublishDiagnosticsParamsImpl(); + params.setUri(doc.getUri()); + params.setDiagnostics(diagnostics); + publishDiagnostics.accept(params); + } + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleWorkspaceService.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleWorkspaceService.java new file mode 100644 index 000000000..8d5b087c2 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/SimpleWorkspaceService.java @@ -0,0 +1,38 @@ +package com.github.kdvolder.lsapi.util; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import io.typefox.lsapi.DidChangeConfigurationParams; +import io.typefox.lsapi.DidChangeWatchedFilesParams; +import io.typefox.lsapi.SymbolInformation; +import io.typefox.lsapi.WorkspaceSymbolParams; +import io.typefox.lsapi.services.WorkspaceService; + +public class SimpleWorkspaceService implements WorkspaceService { + + private ListenerList configurationListeners = new ListenerList<>(); + + @Override + public CompletableFuture> symbol(WorkspaceSymbolParams params) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void didChangeConfiguraton(DidChangeConfigurationParams params) { + configurationListeners.fire(new Settings(params.getSettings())); + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + // TODO Auto-generated method stub + + } + + public void onDidChangeConfiguraton(Consumer l) { + configurationListeners.add(l); + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocument.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocument.java new file mode 100644 index 000000000..212a78ab7 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocument.java @@ -0,0 +1,38 @@ +package com.github.kdvolder.lsapi.util; + +import io.typefox.lsapi.Range; +import io.typefox.lsapi.TextDocumentContentChangeEvent; + +public class TextDocument { + + private final String uri; + private String text = ""; + + public TextDocument(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + public void apply(TextDocumentContentChangeEvent change) { + Range rng = change.getRange(); + if (rng==null) { + //full sync mode + this.text = change.getText(); + } else { + //incremental sync mode + throw new IllegalStateException("Incremental sync not yet implemented"); + } + } + + +} diff --git a/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocumentContentChange.java b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocumentContentChange.java new file mode 100644 index 000000000..b10584e18 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/main/java/com/github/kdvolder/lsapi/util/TextDocumentContentChange.java @@ -0,0 +1,23 @@ +package com.github.kdvolder.lsapi.util; + +import io.typefox.lsapi.TextDocumentContentChangeEvent; + +public class TextDocumentContentChange { + + private final TextDocument document; + private final TextDocumentContentChangeEvent change; + + public TextDocumentContentChange(TextDocument doc, TextDocumentContentChangeEvent change) { + this.document = doc; + this.change = change; + } + + public TextDocument getDocument() { + return document; + } + + public TextDocumentContentChangeEvent getChange() { + return change; + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/ApplicationYamlEditorTest.java b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/ApplicationYamlEditorTest.java new file mode 100644 index 000000000..98f7bc8f7 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/ApplicationYamlEditorTest.java @@ -0,0 +1,52 @@ +package com.github.kdvolder.lsapi.example; + +import org.junit.Test; + +import com.github.kdvolder.lsapi.testharness.Editor; +import com.github.kdvolder.lsapi.testharness.LanguageServerHarness; + +/** + * This class is a placeholder where we will attempt to copy and port + * as many tests a possible from + * org.springframework.ide.eclipse.boot.properties.editor.test.YamlEditorTests + * + * @author Kris De Volder + */ +public class ApplicationYamlEditorTest { + + @Test public void testReconcileCatchesParseError() throws Exception { + LanguageServerHarness harness = new LanguageServerHarness(YamlLanguageServer::new); + harness.intialize(null); + + Editor editor = harness.newEditor( + "somemap: val\n"+ + "- sequence" + ); + editor.assertProblems( + "-|expected " + ); + } + + @Test public void linterRunsOnDocumentOpenAndChange() throws Exception { + LanguageServerHarness harness = new LanguageServerHarness(YamlLanguageServer::new); + harness.intialize(null); + + Editor editor = harness.newEditor( + "somemap: val\n"+ + "- sequence" + ); + + editor.assertProblems( + "-|expected " + ); + + editor.setText( + "- sequence\n" + + "zomemap: val" + ); + + editor.assertProblems( + "z|expected " + ); + } +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/YamlLanguageServerTest.java b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/YamlLanguageServerTest.java new file mode 100644 index 000000000..1fd4e41f3 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/example/YamlLanguageServerTest.java @@ -0,0 +1,74 @@ +package com.github.kdvolder.lsapi.example; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.Test; + +import com.github.kdvolder.lsapi.testharness.LanguageServerHarness; +import com.github.kdvolder.lsapi.testharness.TextDocumentInfo; + +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.InitializeResult; +import io.typefox.lsapi.ServerCapabilities; + +public class YamlLanguageServerTest { + + public static File getTestResource(String name) throws URISyntaxException { + return Paths.get(YamlLanguageServerTest.class.getResource(name).toURI()).toFile(); + } + + @Test + public void createAndInitializeServerWithWorkspace() throws Exception { + LanguageServerHarness harness = new LanguageServerHarness(YamlLanguageServer::new); + File workspaceRoot = getTestResource("/workspace/"); + assertExpectedInitResult(harness.intialize(workspaceRoot)); + } + + @Test + public void createAndInitializeServerWithoutWorkspace() throws Exception { + File workspaceRoot = null; + LanguageServerHarness harness = new LanguageServerHarness(YamlLanguageServer::new); + assertExpectedInitResult(harness.intialize(workspaceRoot)); + } + + + @Test public void completions() throws Exception { + LanguageServerHarness harness = new LanguageServerHarness(YamlLanguageServer::new); + + File workspaceRoot = getTestResource("/workspace/"); + assertExpectedInitResult(harness.intialize(workspaceRoot)); + + TextDocumentInfo doc = harness.openDocument(getTestResource("/workspace/testfile.yml")); + + CompletionList completions = harness.getCompletions(doc, doc.positionOf("foo")); + assertThat(completions.isIncomplete()).isFalse(); + assertThat(completions.getItems()) + .extracting(CompletionItem::getLabel) + .containsExactly("TypeScript", "JavaScript"); + + List resolved = harness.resolveCompletions(completions); + assertThat(resolved) + .extracting(CompletionItem::getLabel) + .containsExactly("TypeScript", "JavaScript"); + + assertThat(resolved) + .extracting(CompletionItem::getDetail) + .containsExactly("TypeScript details", "JavaScript details"); + + assertThat(resolved) + .extracting(CompletionItem::getDocumentation) + .containsExactly("TypeScript docs", "JavaScript docs"); + } + + private void assertExpectedInitResult(InitializeResult initResult) { + assertThat(initResult.getCapabilities().getCompletionProvider().getResolveProvider()).isTrue(); + assertThat(initResult.getCapabilities().getTextDocumentSync()).isEqualTo(ServerCapabilities.SYNC_FULL); + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/Editor.java b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/Editor.java new file mode 100644 index 000000000..c202d649b --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/Editor.java @@ -0,0 +1,131 @@ +package com.github.kdvolder.lsapi.testharness; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.swing.text.BadLocationException; + +import io.typefox.lsapi.Diagnostic; +import io.typefox.lsapi.Position; +import io.typefox.lsapi.PublishDiagnosticsParams; +import io.typefox.lsapi.Range; + +public class Editor { + + private static final Comparator PROBLEM_COMPARATOR = new Comparator() { + @Override + public int compare(Diagnostic o1, Diagnostic o2) { + int diff = compare(o1.getRange().getStart(), o2.getRange().getStart()); + if (diff!=0) return diff; + return compare(o1.getRange().getEnd(), o2.getRange().getEnd()); + } + + private int compare(Position p1, Position p2) { + int d = p1.getLine() - p2.getLine(); + if (d!=0) return d; + return p1.getCharacter() - p2.getCharacter(); + } + }; + + private LanguageServerHarness harness; + private TextDocumentInfo document; + + public Editor(LanguageServerHarness harness, String contents) throws Exception { + this.harness = harness; + this.document = harness.openDocument(harness.createWorkingCopy(contents)); + } + + /** + * Check that a 'expectedProblems' are found by the reconciler. Expected problems are + * specified by string of the form "${badSnippet}|${messageSnippet}". The badSnippet + * is the text expected to be covered by the marker's region and the message snippet must + * be found in the error marker's message. + *

+ * The expected problems are matched one-to-one in the order given (so markers in the + * editor must appear in the expected order for the assert to pass). + * + * @param editor + * @param expectedProblems + * @throws BadLocationException + */ + public void assertProblems(String... expectedProblems) throws Exception { + Editor editor = this; + List actualProblems = new ArrayList<>(editor.reconcile()); + Collections.sort(actualProblems, PROBLEM_COMPARATOR); + String bad = null; + if (actualProblems.size()!=expectedProblems.length) { + bad = "Wrong number of problems (expecting "+expectedProblems.length+" but found "+actualProblems.size()+")"; + } else { + for (int i = 0; i < expectedProblems.length; i++) { + if (!matchProblem(editor, actualProblems.get(i), expectedProblems[i])) { + bad = "First mismatch at index "+i+": "+expectedProblems[i]+"\n"; + break; + } + } + } + if (bad!=null) { + fail(bad+problemSumary(editor, actualProblems)); + } + } + + private String problemSumary(Editor editor, List actualProblems) throws Exception { + StringBuilder buf = new StringBuilder(); + for (Diagnostic p : actualProblems) { + buf.append("\n----------------------\n"); + + String snippet = editor.getText(p.getRange()); + buf.append("("+p.getRange().getStart().getLine()+", "+p.getRange().getStart().getCharacter()+")["+snippet+"]:\n"); + buf.append(" "+p.getMessage()); + } + return buf.toString(); + } + + public String getText(Range range) { + return document.getText(range); + } + + public void setText(String newContent) throws Exception { + document = harness.changeDocument(document.getUri(), newContent); + } + + private boolean matchProblem(Editor editor, Diagnostic problem, String expect) { + String[] parts = expect.split("\\|"); + assertEquals(2, parts.length); + String badSnippet = parts[0]; + String messageSnippet = parts[1]; + boolean spaceSensitive = badSnippet.trim().length() reconcile() { + // We assume the language server works synchronously for now and it does an immediate reconcile + // when the document changes. In the future this is probably not going to be the case though and then this + // method will need to somehow ensure the linter is done working before retrieving the problems from the + // test harness. + PublishDiagnosticsParams diagnostics = harness.getDiagnostics(document); + if (diagnostics!=null) { + return (List) diagnostics.getDiagnostics(); + } + return Collections.emptyList(); + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/LanguageServerHarness.java b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/LanguageServerHarness.java new file mode 100644 index 000000000..38cd865c9 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/LanguageServerHarness.java @@ -0,0 +1,246 @@ +package com.github.kdvolder.lsapi.testharness; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import org.assertj.core.api.Condition; + +import io.typefox.lsapi.ClientCapabilitiesImpl; +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.Diagnostic; +import io.typefox.lsapi.DidChangeTextDocumentParamsImpl; +import io.typefox.lsapi.DidOpenTextDocumentParamsImpl; +import io.typefox.lsapi.InitializeParamsImpl; +import io.typefox.lsapi.InitializeResult; +import io.typefox.lsapi.Position; +import io.typefox.lsapi.PositionImpl; +import io.typefox.lsapi.PublishDiagnosticsParams; +import io.typefox.lsapi.Range; +import io.typefox.lsapi.ServerCapabilities; +import io.typefox.lsapi.TextDocumentContentChangeEventImpl; +import io.typefox.lsapi.TextDocumentItemImpl; +import io.typefox.lsapi.TextDocumentPositionParamsImpl; +import io.typefox.lsapi.VersionedTextDocumentIdentifierImpl; +import io.typefox.lsapi.services.LanguageServer; + +public class LanguageServerHarness { + + //Warning this 'harness' is not very good yet. It just implements bare minimum to + // be able to test the MyLanguageServer example. + + private Random random = new Random(); + + private Callable factory; + + private LanguageServer server; + + private InitializeResult initResult; + + private Map documents = new HashMap<>(); + private Map diagnostics = new HashMap<>(); + + public LanguageServerHarness(Callable factory) throws Exception { + this.factory = factory; + } + + public synchronized TextDocumentInfo getOrReadFile(File file) throws Exception { + String uri = file.toURI().toString(); + TextDocumentInfo d = documents.get(uri); + if (d==null) { + documents.put(uri, d = readFile(file)); + } + return d; + } + + public TextDocumentInfo readFile(File file) throws Exception { + byte[] encoded = Files.readAllBytes(file.toPath()); + String content = new String(encoded, getEncoding()); + TextDocumentItemImpl document = new TextDocumentItemImpl(); + document.setText(content); + document.setUri(file.toURI().toString()); + document.setVersion(getFirstVersion()); + document.setLanguageId(getLanguageId()); + return new TextDocumentInfo(document); + } + + private synchronized TextDocumentItemImpl setDocumentContent(String uri, String newContent) { + TextDocumentInfo o = documents.get(uri); + TextDocumentItemImpl n = new TextDocumentItemImpl(); + n.setLanguageId(o.getLanguageId()); + n.setText(newContent); + n.setVersion(o.getVersion()+1); + n.setUri(o.getUri()); + documents.put(uri, new TextDocumentInfo(n)); + return n; + } + + protected Charset getEncoding() { + return Charset.forName("utf8"); + } + + protected String getLanguageId() { + return "plaintext"; + } + + protected String getFileExtension() { + return ".txt"; + } + + private synchronized void receiveDiagnostics(PublishDiagnosticsParams diags) { + this.diagnostics.put(diags.getUri(), diags); + } + + public InitializeResult intialize(File workspaceRoot) throws Exception { + server = factory.call(); + int parentPid = random.nextInt(40000)+1000; + InitializeParamsImpl initParams = new InitializeParamsImpl(); + initParams.setRootPath(workspaceRoot== null?null:workspaceRoot.toString()); + initParams.setProcessId(parentPid); + ClientCapabilitiesImpl clientCap = new ClientCapabilitiesImpl(); + initParams.setCapabilities(clientCap); + initResult = server.initialize(initParams).get(); + + server.getTextDocumentService().onPublishDiagnostics(this::receiveDiagnostics); + return initResult; + } + + public TextDocumentInfo openDocument(TextDocumentInfo documentInfo) throws Exception { + DidOpenTextDocumentParamsImpl didOpen = new DidOpenTextDocumentParamsImpl(); + didOpen.setTextDocument(documentInfo.getDocument()); + didOpen.setText(documentInfo.getText()); + didOpen.setUri(documentInfo.getUri()); + server.getTextDocumentService().didOpen(didOpen); + return documentInfo; + } + + public TextDocumentInfo openDocument(File file) throws Exception { + return openDocument(getOrReadFile(file)); + } + + public TextDocumentInfo changeDocument(String uri, String newContent) throws Exception { + TextDocumentItemImpl textDocument = setDocumentContent(uri, newContent); + DidChangeTextDocumentParamsImpl didChange = new DidChangeTextDocumentParamsImpl(); + VersionedTextDocumentIdentifierImpl version = new VersionedTextDocumentIdentifierImpl(); + version.setUri(uri); + version.setVersion(textDocument.getVersion()); + didChange.setTextDocument(version); + switch (getDocumentSyncMode()) { + case ServerCapabilities.SYNC_NONE: + break; //nothing todo + case ServerCapabilities.SYNC_INCREMENTAL: + throw new IllegalStateException("Incremental sync not yet supported by this test harness"); + case ServerCapabilities.SYNC_FULL: + TextDocumentContentChangeEventImpl change = new TextDocumentContentChangeEventImpl(); + change.setText(newContent); + didChange.setContentChanges(Collections.singletonList(change)); + break; + default: + throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode()); + } + server.getTextDocumentService().didChange(didChange); + return documents.get(uri); + } + + private int getDocumentSyncMode() { + Integer mode = initResult.getCapabilities().getTextDocumentSync(); + return mode==null ? ServerCapabilities.SYNC_NONE : mode; + } + + public PublishDiagnosticsParams getDiagnostics(TextDocumentInfo doc) { + return diagnostics.get(doc.getUri()); + } + + public static Condition isDiagnosticWithSeverity(int severity) { + return new Condition<>( + (d) -> d.getSeverity()==severity, + "Diagnostic with severity '"+severity+"'" + ); + } + + public static Condition isDiagnosticCovering(TextDocumentInfo doc, String string) { + return new Condition<>( + (d) -> isDiagnosticCovering(d, doc, string), + "Diagnostic covering '"+string+"'" + ); + } + + public static final Condition isWarning = isDiagnosticWithSeverity(Diagnostic.SEVERITY_WARNING); + + public static boolean isDiagnosticCovering(Diagnostic diag, TextDocumentInfo doc, String string) { + Range rng = diag.getRange(); + String actualText = doc.getText(rng); + return string.equals(actualText); + } + + public static Condition isDiagnosticOnLine(int line) { + return new Condition<>( + (d) -> d.getRange().getStart().getLine()==line, + "Diagnostic on line "+line + ); + } + + public CompletionList getCompletions(TextDocumentInfo doc, Position cursor) throws Exception { + TextDocumentPositionParamsImpl params = new TextDocumentPositionParamsImpl(); + params.setPosition(toImpl(cursor)); + params.setTextDocument(doc.getId()); + return server.getTextDocumentService().completion(params).get(); + } + + private PositionImpl toImpl(Position pos) { + if (pos instanceof PositionImpl) { + return (PositionImpl) pos; + } else { + PositionImpl imp = new PositionImpl(); + imp.setCharacter(pos.getCharacter()); + imp.setLine(pos.getLine()); + return imp; + } + } + + private CompletionItem resolveCompletionItem(CompletionItem unresolved) { + try { + return server.getTextDocumentService().resolveCompletionItem(unresolved).get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List resolveCompletions(CompletionList completions) { + return completions.getItems().stream() + .map(this::resolveCompletionItem) + .collect(Collectors.toList()); + } + + public Editor newEditor(String contents) throws Exception { + return new Editor(this, contents); + } + + public synchronized TextDocumentInfo createWorkingCopy(String contents) throws Exception { + TextDocumentItemImpl doc = new TextDocumentItemImpl(); + doc.setLanguageId(getLanguageId()); + doc.setText(contents); + doc.setUri(createTempUri()); + doc.setVersion(getFirstVersion()); + TextDocumentInfo docinfo = new TextDocumentInfo(doc); + documents.put(docinfo.getUri(), docinfo); + return docinfo; + } + + protected int getFirstVersion() { + return 1; + } + + protected String createTempUri() throws Exception { + return File.createTempFile("workingcopy", getFileExtension()).toURI().toString(); + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/TextDocumentInfo.java b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/TextDocumentInfo.java new file mode 100644 index 000000000..9887e6e03 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/java/com/github/kdvolder/lsapi/testharness/TextDocumentInfo.java @@ -0,0 +1,130 @@ +package com.github.kdvolder.lsapi.testharness; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.typefox.lsapi.Position; +import io.typefox.lsapi.PositionImpl; +import io.typefox.lsapi.Range; +import io.typefox.lsapi.TextDocumentIdentifierImpl; +import io.typefox.lsapi.TextDocumentItemImpl; + +public class TextDocumentInfo { + + Pattern NEWLINE = Pattern.compile("\\r|\\n|\\r\\n|\\n\\r"); + + private final TextDocumentItemImpl document; + + private int[] _lineStarts; + + public TextDocumentInfo(TextDocumentItemImpl document) { + this.document = document; + } + + public String getLanguageId() { + return getDocument().getLanguageId(); + } + + public int getVersion() { + return getDocument().getVersion(); + } + + public String getText() { + return getDocument().getText(); + } + + public String getUri() { + return getDocument().getUri(); + } + + public TextDocumentItemImpl getDocument() { + return document; + } + + public String getText(Range rng) { + int start = toOffset(rng.getStart()); + int end = toOffset(rng.getEnd()); + return getText().substring(start, end); + } + + public int toOffset(Position p) { + int startOfLine = startOfLine(p.getLine()); + return startOfLine+p.getCharacter(); + } + + private int startOfLine(int line) { + return lineStarts()[line]; + } + + private int[] lineStarts() { + if (_lineStarts==null) { + _lineStarts = parseLines(); + } + return _lineStarts; + } + + private int[] parseLines() { + List lineStarts = new ArrayList<>(); + lineStarts.add(0); + Matcher matcher = NEWLINE.matcher(getText()); + int pos = 0; + while (matcher.find(pos)) { + lineStarts.add(pos = matcher.end()); + } + int[] array = new int[lineStarts.size()]; + for (int i = 0; i < array.length; i++) { + array[i] = lineStarts.get(i); + } + return array; + } + + /** + * Find and return the (first) position of a given text snippet in the + * document. + * + * @return The position, or null if the snippet can't be found. + */ + public Position positionOf(String snippet) { + int offset = getText().indexOf(snippet); + if (offset>=0) { + return toPosition(offset); + } + return null; + } + + public Position toPosition(int offset) { + int line = lineNumber(offset); + int startOfLine = startOfLine(line); + int column = offset - startOfLine; + PositionImpl pos = new PositionImpl(); + pos.setCharacter(column); + pos.setLine(line); + return pos; + } + + /** + * Determine the line-number a given offset (i.e. what line is the offset inside of?) + */ + private int lineNumber(int offset) { + int[] lineStarts = lineStarts(); + // TODO Could use binary search which is faster + int lineNumber = 0; + for (int i = 0; i < lineStarts.length; i++) { + if (lineStarts[i]<=offset) { + lineNumber = i; + } else { + return lineNumber; + } + } + return lineNumber; + } + + public TextDocumentIdentifierImpl getId() { + TextDocumentIdentifierImpl id = new TextDocumentIdentifierImpl(); + id.setUri(getUri()); + return id; + } + +} diff --git a/vscode-extensions/vscode-application-yaml/src/test/resources/workspace/testfile.yml b/vscode-extensions/vscode-application-yaml/src/test/resources/workspace/testfile.yml new file mode 100644 index 000000000..d76572420 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/src/test/resources/workspace/testfile.yml @@ -0,0 +1,3 @@ +#There's a syntax error in this file +abc: foo +- foo diff --git a/vscode-extensions/vscode-application-yaml/test/.gitignore b/vscode-extensions/vscode-application-yaml/test/.gitignore new file mode 100644 index 000000000..a6c7c2852 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/test/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/vscode-extensions/vscode-application-yaml/test/LintSpec.ts b/vscode-extensions/vscode-application-yaml/test/LintSpec.ts new file mode 100644 index 000000000..e28292577 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/test/LintSpec.ts @@ -0,0 +1,32 @@ +import * as assert from 'assert'; + +// TODO +// describe('lint', () => { +// it('should report a syntax error', done => { +// EMPTY_JAVAC.then(javac => { +// let path = 'test/examples/SyntaxError.java'; + +// return javac.lint({ path }).then(result => { +// let ms = messages(result.messages); + +// assert(ms.length > 0, `${ms} is empty`); + +// done(); +// }); +// }); +// }); + +// it('should report a type error', done => { +// EMPTY_JAVAC.then(javac => { +// let path = 'test/examples/TypeError.java'; + +// return javac.lint({ path }).then(result => { +// let ms = messages(result.messages); + +// assert(ms.length > 0, `${ms} is empty`); + +// done(); +// }); +// }); +// }); +// }); \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/test/examples/application.yml b/vscode-extensions/vscode-application-yaml/test/examples/application.yml new file mode 100644 index 000000000..46d24e076 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/test/examples/application.yml @@ -0,0 +1,2 @@ +foo: bar +- asss diff --git a/vscode-extensions/vscode-application-yaml/test/extension.test.ts b/vscode-extensions/vscode-application-yaml/test/extension.test.ts new file mode 100644 index 000000000..37b717d82 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/test/extension.test.ts @@ -0,0 +1,25 @@ +// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as myExtension from '../lib/Main'; + +// Useful link: +// http://ricostacruz.com/cheatsheets/mocha-tdd.html + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", () => { + + // Defines a Mocha unit test + test("My Extension gets activated", () => { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +}); \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/test/index.ts b/vscode-extensions/vscode-application-yaml/test/index.ts new file mode 100644 index 000000000..e3cebd0d1 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/test/index.ts @@ -0,0 +1,22 @@ +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +var testRunner = require('vscode/lib/testrunner'); + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/tsconfig.json b/vscode-extensions/vscode-application-yaml/tsconfig.json new file mode 100644 index 000000000..afb8e79d2 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "out", + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/tsd.json b/vscode-extensions/vscode-application-yaml/tsd.json new file mode 100644 index 000000000..50cd71dcf --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/tsd.json @@ -0,0 +1,12 @@ +{ + "version": "v4", + "repo": "borisyankov/DefinitelyTyped", + "ref": "master", + "path": "typings", + "bundle": "typings/tsd.d.ts", + "installed": { + "node/node.d.ts": { + "commit": "d22516f9f089de107d7e7d5938566377370631f6" + } + } +} diff --git a/vscode-extensions/vscode-application-yaml/typings/portfinder.d.ts b/vscode-extensions/vscode-application-yaml/typings/portfinder.d.ts new file mode 100644 index 000000000..554fa8db2 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/typings/portfinder.d.ts @@ -0,0 +1,7 @@ + + +declare module 'portfinder' { + var basePort: number; + + function getPort(callback: (err: any, port: number) => void); +} \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/typings/tsd.d.ts b/vscode-extensions/vscode-application-yaml/typings/tsd.d.ts new file mode 100644 index 000000000..2916e4c11 --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/typings/tsd.d.ts @@ -0,0 +1,2 @@ +/// +/// \ No newline at end of file diff --git a/vscode-extensions/vscode-application-yaml/vsc-extension-quickstart.md b/vscode-extensions/vscode-application-yaml/vsc-extension-quickstart.md new file mode 100644 index 000000000..4dfd9da2d --- /dev/null +++ b/vscode-extensions/vscode-application-yaml/vsc-extension-quickstart.md @@ -0,0 +1,33 @@ +# Welcome to your first VS Code Extension + +## What's in the folder +* This folder contains all of the files necessary for your extension +* `package.json` - this is the manifest file in which you declare your extension and command. +The sample plugin registers a command and defines its title and command name. With this information +VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. +* `src/extension.ts` - this is the main file where you will provide the implementation of your command. +The file exports one function, `activate`, which is called the very first time your extension is +activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. +We pass the function containing the implementation of the command as the second parameter to +`registerCommand`. + +## Get up and running straight away +* press `F5` to open a new window with your extension loaded +* run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` +* set breakpoints in your code inside `src/extension.ts` to debug your extension +* find output from your extension in the debug console + +## Make changes +* you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts` +* you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes + +## Explore the API +* you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` + +## Run tests +* open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` +* press `F5` to run the tests in a new window with your extension loaded +* see the output of the test result in the debug console +* make changes to `test/extension.test.ts` or create new test files inside the `test` folder + * by convention, the test runner will only consider files matching the name pattern `**.test.ts` + * you can create folders inside the `test` folder to structure your tests any way you want \ No newline at end of file