Line Decoration for Code Nav (#350)

* update: typescript to latest

* add: ts bundling with ts-loader

* add: KiteCodeLensProvider skeleton

* change: wip codelens -> prototype inline decoration

* update: rm vscode devDep in favor of @types/vscode and vscode-test

See https://code.visualstudio.com/updates/v1_36#_splitting-vscode-package-into-typesvscode-and-vscodetest

* improve: consolidate after block to avoid conflicting styles

Long standing vscode bug "Inline decorations can interfere with one another"
https://github.com/microsoft/vscode/issues/33852

* remove: post-install since now using @types/vscode

* update: webpack and webpack-cli to latest

* migrate: to using vscode-test via webpack transpiling

* improve: fix various tests and improve dev test experience

* improve: use link theme color for inline message

* improve: bump kite-api and use getLineDecoration

* add: source-map and typescript test support

* migrate: expect.js -> chai for assertion style testing

* remove: unused deps + update sinon

* test: codenav-decoration
This commit is contained in:
tonycheang 2021-01-13 11:02:31 -08:00 committed by GitHub
parent 70f28ba42f
commit 28f70f2d31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 704 additions and 243 deletions

View File

@ -12,7 +12,8 @@
"browser": false,
"commonjs": true,
"es6": true,
"node": true
"node": true,
"mocha": true
},
"rules": {
"max-len": "off",

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ sample.html
# bundled assets
dist
out

View File

@ -1,12 +1,16 @@
sudo: false
sudo: required
language: node_js
node_js:
- 10
os:
- linux
before_install:
- if [ $TRAVIS_OS_NAME == "linux" ]; then
export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0;
sh -e /etc/init.d/xvfb start;
export CXX="g++-4.9" CC="gcc-4.9";
export DISPLAY=':99.0'
/usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
sleep 3;
fi
- curl --silent -L "https://s3-us-west-1.amazonaws.com/kite-data/tensorflow/libtensorflow-cpu-linux-x86_64-1.9.0.tar.gz" | tar -C $HOME -xz
@ -20,4 +24,5 @@ install:
script:
- npm test
- LIVE_ENVIRONMENT=1 npm test
# json-runner.test.js is disabled since it needs updating
# - LIVE_ENVIRONMENT=1 npm test

8
.vscode/launch.json vendored
View File

@ -22,12 +22,16 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/test", "--disable-extensions" ],
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test",
"--disable-extensions"
],
"stopOnEntry": false,
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: compile"
"preLaunchTask": "npm: compile-test"
}
]
}

View File

@ -0,0 +1,4 @@
<svg width="420" height="532" viewBox="0 0 420 532" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M220.062 224.958L137.857 379.629L315.934 531.686L374.875 303.883L220.062 224.958Z" fill="#5DD8E4"/>
<path d="M148.303 0L0 261.897L103.229 350.047L203.611 172.788L386.27 259.828L419.99 129.48L148.303 0Z" fill="#5DD8E4"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@ -17,7 +17,7 @@ const config = {
target: 'node',
entry: {
extension: path.resolve(__dirname, '..', 'src', 'kite.js'),
extension: path.resolve(__dirname, '..', 'src', 'kite.js'),
},
output: {
// the bundle is stored in the 'dist' folder (check package.json)
@ -31,8 +31,17 @@ const config = {
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded.
atom: 'atom' // because kite-installer imports it (has null checks around its usage, though)
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.js']
extensions: ['.ts', '.js']
},
plugins: [
// static asset merging and copying
@ -59,4 +68,4 @@ const config = {
})
]
};
module.exports = config;
module.exports = config;

View File

@ -0,0 +1,72 @@
'use strict';
const glob = require('glob');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const CopyPlugin = require('copy-webpack-plugin');
const OUT_TEST_DIR = path.resolve(__dirname, '..', 'out', 'test');
const TEST_DIR = path.resolve(__dirname, '..', 'test');
const TestNeedsUpdating = {
'autostart.test.js': true,
'json-runner.test.js': true,
};
const testEntries = glob
.sync('*.test.{js,ts}', { cwd: TEST_DIR })
.reduce((obj, filename) => {
if (!TestNeedsUpdating[filename]) {
const filenameWithoutExt = filename.replace(path.extname(filename), '');
obj[filenameWithoutExt] = path.resolve(TEST_DIR, filename);
}
return obj;
}, {});
module.exports = {
entry: {
['runTests']: path.resolve(__dirname, '..', 'test', 'runTests.js'),
['index']: path.resolve(__dirname, '..', 'test', 'index.ts'),
...testEntries
},
output: {
path: OUT_TEST_DIR,
filename: '[name].js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
},
devtool: 'source-map',
target: 'node',
externals: [
{
vscode: 'commonjs2 vscode',
fs: 'commonjs2 fs',
crypto: 'commonjs2 crypto',
child_process: 'commonjs2 child_process',
['editors-json-tests']: 'commonjs2 editors-json-tests',
},
nodeExternals()
],
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
plugins: [
new CopyPlugin([
{
from: 'fixtures/',
to: path.resolve(OUT_TEST_DIR, 'fixtures/')
}
],
{ context: TEST_DIR }
)
],
};

View File

@ -5,7 +5,7 @@
"version": "0.135.0",
"publisher": "kiteco",
"engines": {
"vscode": "^1.28.0"
"vscode": "^1.32.0"
},
"icon": "logo.png",
"galleryBanner": {
@ -345,13 +345,18 @@
"type": "array",
"default": [],
"description": "Array of file extensions for which Kite will not provide completions, e.g. ['.go', '.ts']. Requires restart of VSCode."
},
"kite.codefinder.enableLineDecoration": {
"type": "boolean",
"default": true,
"description": "Enables line decoration for Kite code finder."
}
}
}
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test",
"compile-test": "rm -rf ./out/test && webpack --config config/webpack.tests.config.js --mode none",
"test": "npm run compile-test && node ./out/test/runTests.js",
"cleanup": "rm -f package-lock.json && rm -rf node_modules",
"vscode:prepublish": "webpack --config config/webpack.config.js --mode production",
"compile-prod": "webpack --config config/webpack.config.js --mode production",
@ -360,42 +365,44 @@
"install-local": "vsce package && code --install-extension kite-*.vsix && rm kite-*.vsix"
},
"dependencies": {
"analytics-node": "^3.1.1",
"atob": "^2.1.2",
"formidable": "^1.1.1",
"getmac": "^1.2.1",
"kite-api": "=3.18.0",
"kite-api": "=3.19.0",
"kite-connector": "=3.12.0",
"md5": "^2.2.0",
"mixpanel": "^0.5.0",
"open": "^7.3.0",
"rollbar": "^2.3.8",
"tiny-relative-date": "^1.3.0"
"rollbar": "^2.3.8"
},
"devDependencies": {
"@atom/temp": "^0.8.4",
"@types/chai": "^4.2.14",
"@types/md5": "^2.2.1",
"@types/mixpanel": "^2.14.2",
"@types/mocha": "^2.2.32",
"@types/node": "^6.0.40",
"@types/mocha": "^5.2.6",
"@types/node": "^10.12.21",
"@types/sinon": "^9.0.9",
"@types/vscode": "^1.34.0",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"chai": "^4.2.0",
"copy-webpack-plugin": "^5.0.2",
"editors-json-tests": "git://github.com/kiteco/editors-json-tests.git#master",
"eslint": ">=4.18.2",
"expect.js": "^0.3.1",
"fs-plus": "^3.0.2",
"glob": "^7.1.6",
"jsdom": "^10",
"jsdom-global": "^3",
"mocha": "^5.2.0",
"sinon": "^2.3.5",
"mocha": "^6.1.4",
"sinon": "^9.2.2",
"source-map-support": "^0.5.19",
"terser": "^3.17.0",
"typescript": "^2.0.3",
"ts-loader": "^8.0.11",
"typescript": "^4.0.5",
"vsce": "^1.59.0",
"vscode": "^1.1.22",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"vscode-test": "^1.4.1",
"webpack": "^5.10.3",
"webpack-cli": "^4.2.0",
"webpack-merge-and-include-globally": "^2.1.16",
"webpack-node-externals": "^2.5.2",
"widjet-test-utils": "^1.8.0"
}
}

128
src/codenav-decoration.ts Normal file
View File

@ -0,0 +1,128 @@
import * as path from 'path';
import {
DecorationOptions,
DecorationRangeBehavior,
Event,
extensions,
MarkdownString,
Position,
Range,
TextEditor,
TextEditorDecorationType,
TextEditorSelectionChangeEvent,
ThemeColor,
window,
workspace
} from 'vscode';
import * as KiteAPI from "kite-api";
const relatedCodeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
rangeBehavior: DecorationRangeBehavior.ClosedOpen,
});
interface decorationStatusResponse {
inlineMessage: string,
hoverMessage: string,
projectReady: boolean,
}
interface IOnDidChangeTextEditorSelection {
onDidChangeTextEditorSelection: Event<TextEditorSelectionChangeEvent>
}
export default class KiteRelatedCodeDecorationsProvider {
private lineInfo: decorationStatusResponse | undefined
private activeEditor: TextEditor | undefined
constructor(win: IOnDidChangeTextEditorSelection = window) {
this.lineInfo = undefined;
this.activeEditor = undefined;
win.onDidChangeTextEditorSelection(this.onDidChangeTextEditorSelection.bind(this));
}
public dispose(): void {
// Clears all decorations of this type
relatedCodeLineDecoration.dispose();
}
// For testing and easy stubbing
public enabled(): boolean {
return workspace.getConfiguration('kite').codefinder.enableLineDecoration;
}
// Public for testing
public async onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent): Promise<void> {
if (!this.enabled()) {
return;
}
const editor = event.textEditor;
const applicable = this.lineInfo && this.lineInfo.projectReady !== undefined;
const ready = this.lineInfo && this.lineInfo.projectReady;
if (!this.lineInfo || editor !== this.activeEditor || (applicable && !ready)) {
await this.reset(editor);
} else if (event.selections.length != 1) {
await this.reset(editor);
return;
}
if (this.lineInfo && this.lineInfo.projectReady) {
const cursor: Position = event.selections[0].active;
const opts: DecorationOptions = {
hoverMessage: this.hoverMessage(this.lineInfo.hoverMessage),
range: this.lineEnd(cursor),
renderOptions: {
after: {
contentText: `${this.lineInfo.inlineMessage}`,
margin: '0 0 0 3em',
color: new ThemeColor('textLink.activeForeground'),
fontWeight: 'normal',
fontStyle: 'normal',
}
}
};
editor.setDecorations(relatedCodeLineDecoration, [opts]);
}
}
private hoverMessage(hover: string): MarkdownString {
const logo = path.join(extensions.getExtension("kiteco.kite").extensionPath , "/dist/assets/images/logo-light-blue.svg");
const md = new MarkdownString(`![KiteIcon](${logo}|height=10) [${hover}](command:kite.related-code-from-line)`);
// Must mark as trusted to run commands in MarkdownStrings
md.isTrusted = true;
return md;
}
private lineEnd(pos: Position): Range {
const ending = pos.with(pos.line, 9999);
return new Range(ending, ending);
}
private async reset(editor: TextEditor): Promise<void> {
editor.setDecorations(relatedCodeLineDecoration, []);
this.activeEditor = editor;
this.lineInfo = undefined;
const info = await this.fetchDecoration(editor.document.fileName);
if (!info) {
return;
}
this.lineInfo = info;
}
private async fetchDecoration(filename: string): Promise<decorationStatusResponse | null> {
try {
const resp = await KiteAPI.getLineDecoration(filename);
if (resp && !resp.err) {
return {
inlineMessage: resp.inline_message,
hoverMessage: resp.hover_message,
projectReady: resp.project_ready,
} as decorationStatusResponse;
}
} catch (e) {
// pass
}
return null;
}
}

View File

@ -130,11 +130,11 @@ const processCompletion = (
};
export default class KiteCompletionProvider {
constructor(Kite, triggers, optionalTriggers, isTest) {
constructor(Kite, triggers, optionalTriggers, win = window) {
this.Kite = Kite;
this.triggers = triggers;
this.optionalTriggers = optionalTriggers || [];
this.isTest = isTest;
this.window = win;
}
provideCompletionItems(document, position, token, context) {
@ -151,7 +151,7 @@ export default class KiteCompletionProvider {
}
getCompletions(document, text, filename, filterText, context) {
const selection = window.activeTextEditor.selection;
const selection = this.window.activeTextEditor.selection;
const begin = document.offsetAt(selection.start);
const end = document.offsetAt(selection.end);
const enableSnippets = workspace.getConfiguration("kite").enableSnippets;

View File

@ -34,6 +34,7 @@ import {
} from "./utils";
import { version } from "../package.json";
import { DEFAULT_MAX_FILE_SIZE } from "kite-api";
import KiteRelatedCodeDecorationsProvider from './codenav-decoration';
const RUN_KITE_ATTEMPTS = 30;
const RUN_KITE_INTERVAL = 2500;
@ -236,6 +237,8 @@ export const Kite = {
})
);
this.disposables.push(new KiteRelatedCodeDecorationsProvider());
this.disposables.push(
vscode.commands.registerCommand("kite.open-copilot", () => {
kiteOpen("kite://home");

View File

@ -1,14 +1,14 @@
'use strict';
const expect = require('expect.js');
const {kite} = require('../src/kite');
const expect = require('chai').expect;
const kite = require('../src/kite');
const sinon = require('sinon');
const vscode = require('vscode');
const KiteAPI = require('kite-api');
const {withKite} = require('kite-api/test/helpers/kite');
const {waitsFor} = require('./helpers');
const { withKite } = require('kite-api/test/helpers/kite');
const { waitsFor } = require('./helpers');
withKite({running: false}, () => {
withKite({ running: false }, () => {
let spy, spy2;
describe('when startKiteAtStartup is disabled', () => {
@ -18,12 +18,12 @@ withKite({running: false}, () => {
startKiteAtStartup: false,
loggingLevel: 'info',
get(key) {
return this[key]
return this[key];
}
}
};
});
spy = sinon.spy(KiteAPI, 'runKiteAndWait');
kite._activate();
kite.activate({ globalState: {}});
});
afterEach('package deactivation', () => {
@ -44,12 +44,12 @@ withKite({running: false}, () => {
startKiteEngineOnStartup: true,
loggingLevel: 'info',
get(key) {
return this[key]
return this[key];
}
}
};
});
spy = sinon.spy(KiteAPI, 'runKiteAndWait');
kite._activate();
kite.activate({ globalState: {}});
});
afterEach('package deactivation', () => {

View File

@ -0,0 +1,133 @@
import {
DecorationOptions,
MarkdownString,
Position,
Selection,
workspace,
window,
} from 'vscode';
import * as path from 'path';
import { assert } from 'chai';
import * as sinon from 'sinon';
import * as KiteAPI from 'kite-api';
import KiteRelatedCodeDecorationsProvider from '../src/codenav-decoration';
describe('KiteRelatedCodeDecorationsProvider', () => {
it('hooks into the onDidChangeTextEditorSelection callback when initialized', () => {
const onDidChangeTextEditorSelection = sinon.spy();
new KiteRelatedCodeDecorationsProvider({ onDidChangeTextEditorSelection });
assert.isTrue(onDidChangeTextEditorSelection.called);
assert.isFunction(onDidChangeTextEditorSelection.calledWith);
});
describe("for various line decoration API responses", () => {
let setDecorationSpy: sinon.SinonSpy;
let getLineDecorationStub: sinon.SinonStub;
let provider: KiteRelatedCodeDecorationsProvider;
let fireEvent: () => Promise<void>;
beforeEach(async () => {
getLineDecorationStub = sinon.stub(KiteAPI, "getLineDecoration");
provider = new KiteRelatedCodeDecorationsProvider(window);
({ setDecorationSpy, fireEvent } = await setupDocument(provider));
});
afterEach(() => {
getLineDecorationStub.reset();
getLineDecorationStub.restore();
setDecorationSpy.restore();
});
it('sets the decoration when project_ready === true', async () => {
const inlineMessage = "Find related code in kiteco";
const hoverMessage = "Search for related code in kiteco which may be related to this line";
getLineDecorationStub.callsFake(() => {
return {
inline_message: inlineMessage,
hover_message: hoverMessage,
project_ready: true,
};
});
await fireEvent();
const opts: DecorationOptions[] = setDecorationSpy.lastCall.args[1];
assert.isAbove(opts.length, 0, "Last call should include options, which shows the decoration");
assert.include((opts[0].hoverMessage as MarkdownString).value, hoverMessage);
assert.include(opts[0].renderOptions.after.contentText, inlineMessage);
});
it('does not set the decoration when enableLineDecoration === false', async () => {
const getConfigurationStub = sinon.stub(provider, "enabled").callsFake(() => false);
await fireEvent();
assert.isFalse(getLineDecorationStub.called);
assert.isFalse(setDecorationSpy.called);
getConfigurationStub.restore();
});
it('does not set the decoration when project_ready === false', async () => {
getLineDecorationStub.callsFake(() => {
return {
inline_message: "",
hover_message: "",
project_ready: false,
};
});
await fireEvent();
setDecorationSpy.getCalls().forEach(call => {
assert.deepEqual(call.args[1], [], "should have never been called with options");
});
});
it('does not rerequest the decoration when project_ready === undefined', async () => {
getLineDecorationStub.callsFake(() => {
return {
inline_message: "",
hover_message: "",
project_ready: undefined,
};
});
await fireEvent();
assert.isTrue(getLineDecorationStub.calledOnce);
await fireEvent();
assert.isAtMost(getLineDecorationStub.callCount, 1);
setDecorationSpy.getCalls().forEach(call => {
assert.deepEqual(call.args[1], [], "setDecoration should not have been called with options");
});
});
});
});
async function setupDocument(
decorationProvider: KiteRelatedCodeDecorationsProvider
) : Promise<{
setDecorationSpy: sinon.SinonSpy,
fireEvent: () => Promise<void>
}> {
const testDocument = await workspace.openTextDocument(
path.resolve(__dirname, "..", "..", "test", "codenav-decoration.test.ts")
);
const textEditor = await window.showTextDocument(testDocument);
return {
setDecorationSpy: sinon.spy(textEditor, "setDecorations"),
fireEvent: () => {
return decorationProvider.onDidChangeTextEditorSelection({
textEditor,
selections: [
new Selection(
new Position(0,0),
new Position(0,0),
),
]
});
}
};
}

View File

@ -1,25 +1,31 @@
const fs = require('fs');
const expect = require('expect.js');
const vscode = require('vscode');
const {fixtureURI, Kite} = require('./helpers');
import fs from 'fs';
import vscode from 'vscode';
const {withKite, withKiteRoutes} = require('kite-api/test/helpers/kite');
const {fakeResponse} = require('kite-api/test/helpers/http');
import { assert } from 'chai';
import { withKite, withKiteRoutes } from 'kite-api/test/helpers/kite';
import { fakeResponse } from 'kite-api/test/helpers/http';
const KiteCompletionProvider = require('../src/completion');
import { fixtureURI, Kite } from './helpers';
import KiteCompletionProvider from '../src/completion';
const mockWindow = {
activeTextEditor: {
selection: new vscode.Selection(new vscode.Position(19, 13), new vscode.Position(19,13))
}
};
describe('KiteCompletionProvider', () => {
let provider;
beforeEach(() => {
provider = new KiteCompletionProvider(Kite, true);
provider = new KiteCompletionProvider(Kite, ['a'], ['('], mockWindow);
});
withKite({reachable: true}, () => {
withKite({ reachable: true }, () => {
describe('when the endpoints returns some completions', () => {
withKiteRoutes([
[
o => /\/clientapi\/editor\/completions/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('completions.json').toString()))
o => /\/clientapi\/editor\/complete/.test(o.path),
() => fakeResponse(200, fs.readFileSync(fixtureURI('completions.json').toString()))
]
]);
@ -27,17 +33,17 @@ describe('KiteCompletionProvider', () => {
const uri = vscode.Uri.file(fixtureURI('sample.py'));
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideCompletionItems(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res.length).to.eql(2);
.then(doc => provider.provideCompletionItems(doc, new vscode.Position(19, 13), null, { triggerCharacter: '' }))
.then(({ items }) => {
assert.equal(items.length, 2);
expect(res[0].label).to.eql('⟠ dumps');
expect(res[0].insertText).to.eql('idumps');
expect(res[0].sortText).to.eql('0');
assert.equal(items[0].label, 'json.dumps');
assert.equal(items[0].insertText, 'dumps');
assert.equal(items[0].sortText, '0');
expect(res[1].label).to.eql('⟠ dump');
expect(res[1].insertText).to.eql('idump');
expect(res[1].sortText).to.eql('1');
assert.include(items[1].label, 'json.dumps(「obj」)');
assert.equal(items[1].insertText.value, 'dumps(${1:「obj」})$0');
assert.equal(items[1].sortText, '1');
});
});
});
@ -46,18 +52,16 @@ describe('KiteCompletionProvider', () => {
withKiteRoutes([
[
o => /\/clientapi\/editor\/completions/.test(o.path),
o => fakeResponse(404)
() => fakeResponse(404)
]
]);
it('returns null', () => {
it('returns empty array', () => {
const uri = vscode.Uri.file(fixtureURI('sample.py'));
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideCompletionItems(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res).to.eql([]);
});
.then(doc => provider.provideCompletionItems(doc, new vscode.Position(19, 13), null, { triggerCharacter: '' }))
.then(res => assert.deepEqual(res, []));
});
});
});

View File

@ -1,12 +1,12 @@
const fs = require('fs');
const expect = require('expect.js');
const vscode = require('vscode');
const {fixtureURI, Kite} = require('./helpers');
import fs from 'fs';
import vscode from 'vscode';
const {withKite, withKiteRoutes} = require('kite-api/test/helpers/kite');
const {fakeResponse} = require('kite-api/test/helpers/http');
import { expect } from 'chai';
import { withKite, withKiteRoutes } from 'kite-api/test/helpers/kite';
import { fakeResponse } from 'kite-api/test/helpers/http';
const KiteDefinitionProvider = require('../src/definition');
import { fixtureURI, Kite } from './helpers';
import KiteDefinitionProvider from '../src/definition';
describe('KiteDefinitionProvider', () => {
let provider;
@ -14,12 +14,12 @@ describe('KiteDefinitionProvider', () => {
beforeEach(() => {
provider = new KiteDefinitionProvider(Kite, true);
});
withKite({reachable: true}, () => {
withKite({ reachable: true }, () => {
describe('when the endpoints returns a definition', () => {
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment.json').toString()))
() => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment.json').toString()))
]
]);
@ -42,7 +42,7 @@ describe('KiteDefinitionProvider', () => {
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(404)
() => fakeResponse(404)
]
]);

View File

@ -1,22 +1,24 @@
'use strict';
const expect = require('expect.js');
const sinon = require('sinon');
const EditorEvents = require('../src/events');
const vscode = require('vscode');
const {fixtureURI} = require('./helpers');
import vscode from 'vscode';
import { expect } from 'chai';
import sinon from 'sinon';
import EditorEvents from '../src/events';
import { fixtureURI } from './helpers';
describe('EditorEvents', () => {
let editor, events, Kite;
beforeEach(() => {
// We're going to fake most objects that are used by the editor events
// because of how VSCode testing environment works.
// because of how VSCode testing environment works.
// For instance we can't get a reference to the editor of a created document.
Kite = {
request: sinon.stub().returns(Promise.resolve()),
checkState: sinon.stub().returns(Promise.resolve()),
}
};
const uri = vscode.Uri.file(fixtureURI('sample.py'));
@ -28,7 +30,7 @@ describe('EditorEvents', () => {
start: new vscode.Position(0,0),
end: new vscode.Position(0,0),
},
}
};
events = new EditorEvents(Kite, editor);
});
@ -41,10 +43,10 @@ describe('EditorEvents', () => {
])
.then(() => {
expect(Kite.request.callCount).to.eql(1);
const [, json] = Kite.request.getCall(0).args;
const payload = JSON.parse(json);
expect(payload.action).to.eql('edit')
expect(payload.action).to.eql('edit');
});
});
});

View File

@ -1,32 +1,49 @@
{
"language": "python",
"offset_encoding": "utf-32",
"completions": [
{
"display": "dumps",
"insert": "idumps",
"snippet": {
"text": "dumps",
"placeholders": []
},
"replace": {
"begin": 17,
"end": 18
},
"display": "json.dumps",
"web_id": "json.dumps",
"local_id": "python;;;;json.dumps",
"hint": "function",
"id": "json.dumps",
"documentation_text": "some dumps documentation",
"symbol": {
"value": [
{
"repr": "json.dumps"
}
]
}
}, {
"display": "dump",
"insert": "idump",
"hint": "function",
"id": "json.dump",
"documentation_text": "some dump documentation",
"symbol": {
"value": [
{
"repr": "json.dump"
}
]
}
"documentation": {
"text": "..."
},
"smart": false,
"children": [
{
"snippet": {
"text": "dumps(「obj」)",
"placeholders": [{
"begin": 6,
"end": 11
}]
},
"replace": {
"begin": 17,
"end": 18
},
"display": "json.dumps(「obj」)",
"hint": "function",
"documentation": {
"text": "..."
},
"smart": true
}
]
}
]
}
}

View File

@ -1,16 +1,8 @@
"use strict";
const path = require("path");
const sinon = require("sinon");
const Logger = require("kite-connector/lib/logger");
const KiteAPI = require("kite-api");
const { promisifyReadResponse } = require("../src/utils");
const { withKiteRoutes } = require("kite-api/test/helpers/kite");
const { fakeResponse } = require("kite-api/test/helpers/http");
before(() => {
sinon.stub(Logger, "log");
});
const Kite = {
request(req, data) {

View File

@ -1,11 +1,12 @@
const fs = require('fs');
const expect = require('expect.js');
const vscode = require('vscode');
const {fixtureURI, Kite} = require('./helpers');
import fs from 'fs';
import vscode from 'vscode';
const {withKite, withKiteRoutes} = require('kite-api/test/helpers/kite');
const {fakeResponse} = require('kite-api/test/helpers/http');
const KiteHoverProvider = require('../src/hover');
import { assert } from 'chai';
import { withKite, withKiteRoutes } from 'kite-api/test/helpers/kite';
import { fakeResponse } from 'kite-api/test/helpers/http';
import { fixtureURI, Kite } from './helpers';
import KiteHoverProvider from '../src/hover';
describe('KiteHoverProvider', () => {
let provider;
@ -13,12 +14,12 @@ describe('KiteHoverProvider', () => {
beforeEach(() => {
provider = new KiteHoverProvider(Kite, true);
});
withKite({reachable: true}, () => {
withKite({ reachable: true }, () => {
describe('for a python function with a definition', () => {
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment.json').toString()))
() => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment.json').toString()))
]
]);
@ -27,10 +28,12 @@ describe('KiteHoverProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideHover(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res.contents.length).to.eql(1);
.then(({ contents }) => {
assert.equal(contents.length, 1);
const contentString = contents[0].value;
// TODO(Daniel): Content tests
assert.include(contentString, '[Docs](command:kite.more-position?{"position":{"line":19,"character":13},"source":"Hover"}');
assert.include(contentString, '[Def](command:kite.def?{"file":"sample.py","line":50,"source":"Hover"})');
});
});
});
@ -39,7 +42,7 @@ describe('KiteHoverProvider', () => {
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment-no-id-no-def.json').toString()))
() => fakeResponse(200, fs.readFileSync(fixtureURI('test/increment-no-id-no-def.json').toString()))
]
]);
@ -48,19 +51,21 @@ describe('KiteHoverProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideHover(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res.contents.length).to.eql(1);
.then(({ contents }) => {
assert.equal(contents.length, 1);
const contentString = contents[0].value;
// TODO(Daniel): Content tests
assert.include(contentString, '[Docs](command:kite.more-position?{"position":{"line":19,"character":13},"source":"Hover"}');
});
});
});
describe('for a python module', () => {
const osjson = fs.readFileSync(fixtureURI('os.json').toString());
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('os.json').toString()))
() => fakeResponse(200, osjson)
]
]);
@ -69,17 +74,26 @@ describe('KiteHoverProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideHover(doc, new vscode.Position(19, 13), null))
.then(res => {
// TODO(Daniel): Fill in tests
.then(({ contents }) => {
assert.equal(contents.length, 1);
const contentString = contents[0].value;
assert.include(contentString, '[Docs](command:kite.more-position?{"position":{"line":19,"character":13},"source":"Hover"}');
const data = JSON.parse(osjson);
data["symbol"][0]["value"].forEach(({ type }) => {
assert.include(contentString, type);
});
});
});
});
describe('for an instance', () => {
const selfjson = fs.readFileSync(fixtureURI('self.json').toString());
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('self.json').toString()))
() => fakeResponse(200, selfjson)
]
]);
@ -88,8 +102,17 @@ describe('KiteHoverProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideHover(doc, new vscode.Position(19, 13), null))
.then(res => {
// TODO(Daniel): Fill in tests
.then(({ contents }) => {
assert.equal(contents.length, 1);
const contentString = contents[0].value;
assert.include(contentString, "[Docs](command:kite.more-position");
assert.include(contentString, '"position":{"line":19,"character":13}');
const data = JSON.parse(selfjson);
data["symbol"][0]["value"].forEach(({ type }) => {
assert.include(contentString, type);
});
});
});
});
@ -98,7 +121,7 @@ describe('KiteHoverProvider', () => {
withKiteRoutes([
[
o => /\/api\/buffer\/vscode\/.*\/hover/.test(o.path),
o => fakeResponse(404)
() => fakeResponse(404)
]
]);
@ -107,9 +130,7 @@ describe('KiteHoverProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideHover(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res).to.be(undefined);
});
.then(res => assert.equal(res, undefined));
});
});
});

View File

@ -1,25 +0,0 @@
//
// 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.
process.env.NODE_ENV = "test";
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: 'bdd', // the TDD UI is being used in extension.test.js (suite, test, etc.)
useColors: true, // colored output from test results
timeout: 5000,
});
module.exports = testRunner;

42
test/index.ts Normal file
View File

@ -0,0 +1,42 @@
// This file provides the test runner to use when running extension tests,
// based off the example in VSCode documentation.
process.env.NODE_ENV = "test";
import 'source-map-support/register';
import * as path from 'path';
import * as glob from 'glob';
import Mocha = require('mocha')
export function run(): Promise<void> {
const mocha = new Mocha({
ui: 'bdd', // the TDD UI is being used in extension.test.js (suite, test, etc.)
timeout: 5000,
});
mocha.useColors(true);
const outTestDir = path.resolve(__dirname);
return new Promise((res, rej) => {
glob('*.test.js', { cwd: outTestDir }, (err, files) => {
if (err) {
return rej(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(outTestDir, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
rej(new Error(`${failures} tests failed.`));
} else {
res();
}
});
} catch (err) {
rej(err);
}
});
});
}

View File

@ -1,37 +1,37 @@
'use strict';
const expect = require('expect.js')
const expect = require('chai').expect;
const vscode = require('vscode');
const {substituteFromContext, buildContext, itForExpectation, NotificationsMock} = require('../utils');
const {waitsFor} = require('../../helpers')
const { substituteFromContext, buildContext, itForExpectation, NotificationsMock } = require('../utils');
const { waitsFor } = require('../../helpers');
module.exports = ({expectation, not, root}) => {
module.exports = ({ expectation, not, root }) => {
beforeEach(() => {
const spy = vscode.window[NotificationsMock.LEVELS[expectation.properties.level]]
const spy = vscode.window[NotificationsMock.LEVELS[expectation.properties.level]];
const promise = waitsFor(`${expectation.properties.level} notification`, () => {
return NotificationsMock.newNotification();
}, 100)
}, 100);
if(not) {
return promise.then(() => {
throw new Error(`no ${expectation.properties.level} notification, but some were found`)
}, () => {})
throw new Error(`no ${expectation.properties.level} notification, but some were found`);
}, () => {});
} else {
return promise
return promise;
}
});
const block = () => {
if(!not) {
expect(NotificationsMock.lastNotification.level).to.eql(expectation.properties.level)
expect(NotificationsMock.lastNotification.level).to.eql(expectation.properties.level);
if(expectation.properties.message) {
const message = substituteFromContext(expectation.properties.message, buildContext(root))
const message = substituteFromContext(expectation.properties.message, buildContext(root));
expect(NotificationsMock.lastNotification.message).to.eql(message);
}
}
}
};
itForExpectation(expectation, block);
}
};

View File

@ -1,17 +1,17 @@
'use strict';
const expect = require('expect.js');
const {itForExpectation, NotificationsMock} = require('../utils');
const expect = require('chai').expect;
const { itForExpectation, NotificationsMock } = require('../utils');
module.exports = ({expectation, not}) => {
module.exports = ({ expectation, not }) => {
const block = () => {
if(not) {
expect(NotificationsMock.notificationsForLevel(expectation.properties.level).length).not.to.eql(expectation.properties.count)
expect(NotificationsMock.notificationsForLevel(expectation.properties.level).length).not.to.eql(expectation.properties.count);
} else {
expect(NotificationsMock.notificationsForLevel(expectation.properties.level).length).to.eql(expectation.properties.count)
expect(NotificationsMock.notificationsForLevel(expectation.properties.level).length).to.eql(expectation.properties.count);
}
}
};
itForExpectation(expectation, block);
}
};

View File

@ -1,14 +1,11 @@
'use strict';
const expect = require('expect.js')
const vscode = require('vscode');
const http = require('http');
const expect = require('chai').expect;
const KiteAPI = require('kite-api');
const KiteConnect = require('kite-connector');
const {loadPayload, substituteFromContext, buildContext, itForExpectation, inLiveEnvironment} = require('../utils');
const {waitsFor, formatCall} = require('../../helpers')
const { loadPayload, substituteFromContext, buildContext, itForExpectation, inLiveEnvironment } = require('../utils');
const { waitsFor, formatCall } = require('../../helpers');
let closeMatches, calls;
let closeMatches;
const getDesc = (expectation, root) => () => {
const base = [
'request to',
@ -20,21 +17,21 @@ const getDesc = (expectation, root) => () => {
if(expectation.properties.body) {
base.push('with');
base.push(JSON.stringify(substituteFromContext(loadPayload(expectation.properties.body), buildContext(root))))
base.push(JSON.stringify(substituteFromContext(loadPayload(expectation.properties.body), buildContext(root))));
}
if (closeMatches.length > 0) {
base.push('\nbut some calls were close');
closeMatches.forEach((call) => {
base.push(`\n - ${formatCall(call)}`)
base.push(`\n - ${formatCall(call)}`);
});
} else {
// .map(({args: [{path, method}, payload]}) => `${method || 'GET'} ${path} '${payload || ''}'`));
base.push(`\nbut no calls were anywhere close\n${KiteAPI.request.getCalls().map(c => {
let [{path, method}, payload] = c.args;
let [{ path, method }, payload] = c.args;
method = method || 'GET';
return `- ${formatCall({path, method, payload})}`
return `- ${formatCall({ path, method, payload })}`;
}).join('\n')}`);
}
@ -51,7 +48,7 @@ const getNotDesc = (expectation, root) => {
if(expectation.properties.body) {
base.push('with');
base.push(JSON.stringify(substituteFromContext(loadPayload(expectation.properties.body), buildContext(root))))
base.push(JSON.stringify(substituteFromContext(loadPayload(expectation.properties.body), buildContext(root))));
}
base.push('\nbut calls were found');
@ -79,7 +76,7 @@ const mostRecentCallMatching = (data, exPath, exMethod, exPayload, context = {},
if (calls.length === 0) { return false; }
return calls.reverse().reduce((b, c, i, a) => {
let {path, method, body} = c;
let { path, method, body } = c;
method = method || 'GET';
// b is false here only if we found a call that partially matches
@ -92,7 +89,7 @@ const mostRecentCallMatching = (data, exPath, exMethod, exPayload, context = {},
if (path === exPath) {
if (method === exMethod) {
closeMatches.push({path, method, body});
closeMatches.push({ path, method, body });
if (!exPayload || expect.eql(JSON.parse(body), exPayload)) {
matched = true;
return true;
@ -114,11 +111,11 @@ const mostRecentCallMatching = (data, exPath, exMethod, exPayload, context = {},
}, true);
};
module.exports = ({expectation, not, root}) => {
module.exports = ({ expectation, not, root }) => {
beforeEach('request matching', function() {
const promise = waitsFor(getDesc(expectation, root), () => {
if (inLiveEnvironment()) {
return KiteAPI.requestJSON({path: '/testapi/request-history'})
return KiteAPI.requestJSON({ path: '/testapi/request-history' })
.then((data) => {
if (!mostRecentCallMatching(
data,
@ -144,7 +141,7 @@ module.exports = ({expectation, not, root}) => {
if(not) {
return promise.then(() => {
throw new Error(getNotDesc(expectation, root));
}, () => {})
}, () => {});
} else {
return promise;
}

View File

@ -1,10 +1,8 @@
'use strict';
const expect = require('expect.js')
const vscode = require('vscode');
const http = require('http');
const {loadPayload, substituteFromContext, buildContext, itForExpectation, inLiveEnvironment} = require('../utils');
const {waitsFor} = require('../../helpers')
const expect = require('chai').expect;
const { loadPayload, substituteFromContext, buildContext, itForExpectation, inLiveEnvironment } = require('../utils');
const { waitsFor } = require('../../helpers');
const KiteAPI = require('kite-api');
const callsMatching = (data, exPath, exMethod, exPayload, context={}) => {
@ -16,7 +14,7 @@ const callsMatching = (data, exPath, exMethod, exPayload, context={}) => {
};
});
exPath = substituteFromContext(exPath, context)
exPath = substituteFromContext(exPath, context);
exPayload = exPayload && substituteFromContext(loadPayload(exPayload), context);
// console.log('--------------------')
@ -25,20 +23,20 @@ const callsMatching = (data, exPath, exMethod, exPayload, context={}) => {
if(calls.length === 0) { return false; }
return calls.reverse().filter((c) => {
let {path, method, body} = c;
method = method || 'GET'
let { path, method, body } = c;
method = method || 'GET';
// console.log(path, method, payload)
return path === exPath && method === exMethod && (!exPayload || expect.eql(JSON.parse(body), exPayload))
return path === exPath && method === exMethod && (!exPayload || expect.eql(JSON.parse(body), exPayload));
});
}
};
module.exports = ({expectation, not, root}) => {
module.exports = ({ expectation, not, root }) => {
beforeEach('request count', () => {
const promise = waitsFor(`${expectation.properties.count} requests to '${expectation.properties.path}' for test '${expectation.description}'`, () => {
if (inLiveEnvironment()) {
return KiteAPI.requestJSON({path: '/testapi/request-history'})
return KiteAPI.requestJSON({ path: '/testapi/request-history' })
.then((data) => {
const calls = callsMatching(
data,
@ -61,7 +59,7 @@ module.exports = ({expectation, not, root}) => {
return calls.length === expectation.properties.count;
}
}, 3000)
}, 3000)
.catch(err => {
console.log(err);
throw err;
@ -75,7 +73,7 @@ module.exports = ({expectation, not, root}) => {
expectation.properties.body,
buildContext(root)).length;
throw new Error(`no ${expectation.properties.count} requests to '${expectation.properties.path}' for test '${expectation.description}' but ${callsCount} were found`);
}, () => {})
}, () => {});
} else {
return promise;
}

32
test/runTests.js Normal file
View File

@ -0,0 +1,32 @@
import * as path from 'path';
import { runTests, downloadAndUnzipVSCode } from 'vscode-test';
async function main() {
try {
const __dirname = path.resolve(path.dirname(''));
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '.');
// The path to the extension test runner script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './out/test');
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
console.log("Finished downloading VSCode to ", vscodeExecutablePath);
const exitCode = await runTests({
vscodeExecutablePath,
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: ['--disable-extensions']
});
console.log("Finished running tests with exit code", exitCode);
} catch (err) {
console.error('Failed to run tests: ', err);
process.exit(1);
}
}
main();

View File

@ -1,13 +1,13 @@
const fs = require('fs');
const expect = require('expect.js');
const expect = require('chai').expect;
const vscode = require('vscode');
const {fixtureURI, Kite} = require('./helpers');
const { fixtureURI, Kite } = require('./helpers');
const {withKite, withKiteRoutes} = require('kite-api/test/helpers/kite');
const {fakeResponse} = require('kite-api/test/helpers/http');
const { withKite, withKiteRoutes } = require('kite-api/test/helpers/kite');
const { fakeResponse } = require('kite-api/test/helpers/http');
const KiteSignatureProvider = require('../src/signature');
const KiteSignatureProvider = require('../src/signature').default;
describe('KiteSignatureProvider', () => {
let provider;
@ -15,12 +15,12 @@ describe('KiteSignatureProvider', () => {
beforeEach(() => {
provider = new KiteSignatureProvider(Kite, true);
});
withKite({reachable: true}, () => {
withKite({ reachable: true }, () => {
describe('for a python function with a signature', () => {
withKiteRoutes([
[
o => /\/clientapi\/editor\/signatures/.test(o.path),
o => fakeResponse(200, fs.readFileSync(fixtureURI('plot-signatures.json').toString()))
() => fakeResponse(200, fs.readFileSync(fixtureURI('plot-signatures.json').toString()))
]
]);
@ -30,14 +30,14 @@ describe('KiteSignatureProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideSignatureHelp(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res.signatures.length).to.eql(1);
expect(res.signatures[0].label).to.eql('⟠ plot(x:list|uint64, y:list|str)');
expect(res.signatures[0].parameters.length).to.eql(2);
expect(res.signatures[0].parameters[0].label).to.eql('x:list|uint64');
expect(res.signatures[0].parameters[1].label).to.eql('y:list|str');
expect(res.signatures.length).to.equal(1);
expect(res.signatures[0].label).to.equal('⟠ plot(x:list|uint64, y:list|str)');
expect(res.signatures[0].parameters.length).to.equal(2);
expect(res.signatures[0].parameters[0].label).to.equal('x:list|uint64');
expect(res.signatures[0].parameters[1].label).to.equal('y:list|str');
expect(res.activeParameter).to.eql(1);
expect(res.activeSignature).to.eql(0);
expect(res.activeParameter).to.equal(1);
expect(res.activeSignature).to.equal(0);
});
});
});
@ -46,7 +46,7 @@ describe('KiteSignatureProvider', () => {
withKiteRoutes([
[
o => /\/clientapi\/editor\/signatures/.test(o.path),
o => fakeResponse(404)
() => fakeResponse(404)
]
]);
@ -56,7 +56,7 @@ describe('KiteSignatureProvider', () => {
return vscode.workspace.openTextDocument(uri)
.then(doc => provider.provideSignatureHelp(doc, new vscode.Position(19, 13), null))
.then(res => {
expect(res).to.be(null);
expect(res).to.equal(null);
});
});
});

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"moduleResolution": "node",
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "commonjs",
"target": "es6",
"allowJs": true
},
"exclude": [
"node_modules"
]
}