() {
+ @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 extends LanguageServer> factory;
+
+ private LanguageServer server;
+
+ private InitializeResult initResult;
+
+ private Map documents = new HashMap<>();
+ private Map diagnostics = new HashMap<>();
+
+ public LanguageServerHarness(Callable extends LanguageServer> 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