Local LSP server that improves functionality like item completion (#122)
* Initial commit with LSP from MS examples * Code quality fixes * added remote LSP server to run parallel to local LSP server * removed lspEnabled check as its done in extension already * Removed completion from REST * removed comments * Turned Server into a class and extracted validation * Removed log output * WIP: Added items completion in LSP server Items are taken from REST API at start and are getting cached in LSP Server. Their values are updated from SSE (/rest/events). When Items are added or removed the cache is updated. So now we have a very responsive completion list. * Code quality changes * made code more robust * Added parsing of StateChange and uItemUdate events * Fixed wrongly called cb(error) * removed log output * Cleaned up validation to not return anything * Cleaned up and added bit more docs * Fixed capital letter of class in import - worked on win but not in linux * Same as before * Removed unused getter * Code Quality * Moved @types dependencies to root package.json and cleaned up tsconfig files * Test commit for sign off * Added author in docs * Added pure JS impl of server and wrote tests with jest Coverage is not goot yet, ItemCompletionProvider and Item still miss some tests. Other files are good already. * Added more tests, use of preomise instead of callbacks in completionitem * Moved tests to unit folder * Added more tests * Cleaned up and removed TS impl * Fixed compile problems by increasing vscode version * Fallback to empty array if no items map is present * More tests * Improved npm scripts npm run build builds a .vsix npm run publish publishes the extension both commands run tests before to make sure a working version is built/deployed * use bind() instead of anonymous function * improved scripts * Renamed config properties and removed useRestCompletions as its not needed anymre * changed config in remote language client * removed TODO comment * added changelog and fixed description for settings * fixed typo * Removed commented code Signed-off-by: Samuel Brucksch <sasliga@freenet.de> (github: SamuelBrucksch)pull/136/head
parent
82afd6fdfc
commit
91e7770c25
|
@ -2,4 +2,9 @@ out
|
|||
node_modules
|
||||
*.vsix
|
||||
*.todo
|
||||
*.zip
|
||||
*.zip
|
||||
**/out
|
||||
**/node_modules
|
||||
.vscode
|
||||
.vscode-test
|
||||
coverage
|
|
@ -10,7 +10,7 @@
|
|||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/out/src/**/*.js" ],
|
||||
"outFiles": [ "${workspaceRoot}/**/out/**/*.js" ],
|
||||
"preLaunchTask": "npm"
|
||||
},
|
||||
{
|
||||
|
@ -23,6 +23,19 @@
|
|||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
|
||||
"preLaunchTask": "npm"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"name": "lsp-server-jest-tests",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/serverJS/node_modules/jest/bin/jest",
|
||||
"args": [
|
||||
"--runInBand",
|
||||
"--detectOpenHandles"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/serverJS",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
# openHAB VS Code Extension Change Log
|
||||
|
||||
## 0.5.0 - TBD
|
||||
- Added local LSP server (#122)
|
||||
- Fixed sorting order in items explorer (#125)
|
||||
- removed settings param 'restCompletions'
|
||||
- renamed settings param 'lspEnabled' to 'remoteLspEnabled'
|
||||
- renamed settings param 'lspPort' to 'remoteLspPort'
|
||||
|
||||
## 0.4.1 - 2018-12-09
|
||||
- Fixed Basic UI Preview (#117)
|
||||
- Fixed Show in Paper UI command (#117)
|
||||
|
|
|
@ -85,14 +85,14 @@ In the unlikely case that your language server is running on a port other than t
|
|||
|
||||
```json
|
||||
{
|
||||
"openhab.lspPort": 5007
|
||||
"openhab.remoteLspPort": 5007
|
||||
}
|
||||
```
|
||||
|
||||
If you don't want to have your openHAB files validated by Language Server, simply disable it in the extension:
|
||||
```json
|
||||
{
|
||||
"openhab.lspEnabled": false
|
||||
"openhab.remoteLspEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -107,7 +107,7 @@ The following configuration will allow you to access REST API remotely:
|
|||
```
|
||||
"openhab.host": "https://home.myopenhab.org",
|
||||
"openhab.port": 80,
|
||||
"openhab.lspEnabled": false,
|
||||
"openhab.remoteLspEnabled": false,
|
||||
"openhab.username": "your_myopenhab_email",
|
||||
"openhab.password": "your_myopenhab_password",
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "openhab-vscode-extension",
|
||||
"displayName": "openHAB Visual Studio Code Extension",
|
||||
"description": "Robust tool for openHAB textual configurations. Includes code snippets, syntax highlighting, language server integration and more.",
|
||||
"version": "0.4.1",
|
||||
"publisher": "openhab",
|
||||
"icon": "../openhab.png",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openhab/openhab-vscode.git"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"engines": {
|
||||
"vscode": "^1.30.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "vscode-install",
|
||||
"test": "echo \"No tests in client yet!\""
|
||||
},
|
||||
"dependencies": {
|
||||
"ascii-table": "0.0.9",
|
||||
"copy-paste": "^1.3.0",
|
||||
"lodash": "^4.17.4",
|
||||
"request": "^2.83.0",
|
||||
"request-promise-native": "^1.0.5",
|
||||
"underscore.string": "^3.3.5",
|
||||
"vscode": "^1.1.26",
|
||||
"vscode-languageclient": "^5.2.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
Disposable,
|
||||
workspace
|
||||
} from 'vscode'
|
||||
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
TransportKind,
|
||||
ServerOptions
|
||||
} from 'vscode-languageclient'
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* @author Samuel Brucksch
|
||||
*/
|
||||
export class LocalLanguageClientProvider {
|
||||
constructor() { }
|
||||
|
||||
public connect(context): Disposable {
|
||||
// The debug options for the server
|
||||
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
|
||||
const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
|
||||
|
||||
const serverModule = context.asAbsolutePath(path.join("serverJS", "src", "LSPServer.js"));
|
||||
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: ServerOptions = {
|
||||
run: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
},
|
||||
debug: {
|
||||
module: serverModule,
|
||||
transport: TransportKind.ipc,
|
||||
options: debugOptions,
|
||||
},
|
||||
};
|
||||
|
||||
const extensions = ["things", "items", "rules", "script", "sitemap", "persist"];
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ scheme: "file", language: "openhab", pattern: `**/*.{${extensions.join(",")}}` }],
|
||||
synchronize: {
|
||||
configurationSection: "openhab",
|
||||
fileEvents: workspace.createFileSystemWatcher("**/.clientrc"),
|
||||
},
|
||||
};
|
||||
|
||||
// Create the language client and start the client.
|
||||
const lc = new LanguageClient("openhabLanguageServer", "Openhab Language Server", serverOptions, clientOptions);
|
||||
return lc.start();
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import {
|
|||
LanguageClientOptions
|
||||
} from 'vscode-languageclient'
|
||||
|
||||
export class LanguageClientProvider {
|
||||
export class RemoteLanguageClientProvider {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ export class LanguageClientProvider {
|
|||
let config = workspace.getConfiguration('openhab')
|
||||
let connectionInfo = {
|
||||
host: config.host,
|
||||
port: config.lspPort
|
||||
port: config.remoteLspPort
|
||||
}
|
||||
|
||||
let extensions = [
|
||||
|
@ -44,16 +44,15 @@ export class LanguageClientProvider {
|
|||
}
|
||||
|
||||
let clientOptions: LanguageClientOptions = {
|
||||
documentSelector: ['openhab'],
|
||||
documentSelector: [{ scheme: "file", language: "openhab", pattern: `**/*.{${extensions.join(",")}}` }],
|
||||
synchronize: {
|
||||
configurationSection: 'openhabLSP',
|
||||
fileEvents: workspace.createFileSystemWatcher('**/*.{' + extensions.join(',') + '}')
|
||||
configurationSection: "openhab",
|
||||
fileEvents: workspace.createFileSystemWatcher("**/.clientrc"),
|
||||
}
|
||||
}
|
||||
|
||||
if (config.useRestApi) {
|
||||
let lc = new LanguageClient('openHABlsp', 'openHAB Server', serverOptions, clientOptions)
|
||||
return lc.start()
|
||||
}
|
||||
// Create the language client and start the client.
|
||||
let lc = new LanguageClient('openHABlsp', 'openHAB Server', serverOptions, clientOptions)
|
||||
return lc.start()
|
||||
}
|
||||
}
|
|
@ -22,7 +22,8 @@ import { ItemsProvider } from './ThingsExplorer/ItemsProvider'
|
|||
import { ItemsCompletion } from './ItemsExplorer/ItemsCompletion'
|
||||
import { RuleProvider } from './ItemsExplorer/RuleProvider'
|
||||
import { SitemapPartialProvider } from './ItemsExplorer/SitemapPartialProvider'
|
||||
import { LanguageClientProvider } from './LanguageClient/LanguageClientProvider'
|
||||
import { LocalLanguageClientProvider } from './LanguageClient/LocalLanguageClientProvider'
|
||||
import { RemoteLanguageClientProvider } from './LanguageClient/RemoteLanguageClientProvider'
|
||||
import { Item } from './ItemsExplorer/Item'
|
||||
import { Thing } from './ThingsExplorer/Thing'
|
||||
import { Channel } from './ThingsExplorer/Channel'
|
||||
|
@ -33,7 +34,7 @@ import * as path from 'path'
|
|||
|
||||
let _extensionPath: string;
|
||||
|
||||
async function init(disposables: Disposable[], config): Promise<void> {
|
||||
async function init(disposables: Disposable[], config, context): Promise<void> {
|
||||
|
||||
disposables.push(commands.registerCommand('openhab.basicUI', () => {
|
||||
let editor = window.activeTextEditor
|
||||
|
@ -162,16 +163,15 @@ async function init(disposables: Disposable[], config): Promise<void> {
|
|||
|
||||
disposables.push(commands.registerCommand('openhab.command.things.copyUID', (query) =>
|
||||
ncp.copy(query.UID || query.uid)))
|
||||
|
||||
if (config.restCompletions) {
|
||||
disposables.push(languages.registerCompletionItemProvider('openhab', itemsCompletion))
|
||||
}
|
||||
}
|
||||
|
||||
if (config.lspEnabled) {
|
||||
const languageClientProvider = new LanguageClientProvider()
|
||||
disposables.push(languageClientProvider.connect())
|
||||
if (config.remoteLspEnabled) {
|
||||
const remoteLanguageClientProvider = new RemoteLanguageClientProvider()
|
||||
disposables.push(remoteLanguageClientProvider.connect())
|
||||
}
|
||||
|
||||
const localLanguageClientProvider = new LocalLanguageClientProvider()
|
||||
disposables.push(localLanguageClientProvider.connect(context))
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
|
@ -180,7 +180,7 @@ export function activate(context: ExtensionContext) {
|
|||
let config = workspace.getConfiguration('openhab')
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()))
|
||||
|
||||
init(disposables, config)
|
||||
init(disposables, config, context)
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"lib": ["es6"],
|
||||
"types":["node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"rules": {
|
||||
"no-unused-expression": true,
|
||||
"no-duplicate-variable": true,
|
||||
"curly": true,
|
||||
"class-name": true,
|
||||
"semicolon": ["never"],
|
||||
"triple-equals": true
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"engines": {
|
||||
"vscode": "^1.28.0"
|
||||
"vscode": "^1.30.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"onCommand:openhab.command.things.addItems",
|
||||
"onLanguage:openhab"
|
||||
],
|
||||
"main": "./out/src/extension",
|
||||
"main": "./client/out/extension",
|
||||
"contributes": {
|
||||
"menus": {
|
||||
"editor/title": [
|
||||
|
@ -171,7 +171,7 @@
|
|||
"openhab.useRestApi": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Connects to openHAB REST API if set to true. If not, Items tree view and code completions are disabled."
|
||||
"description": "Connects to openHAB REST API if set to true. If not, Items tree view and things tree view are disabled."
|
||||
},
|
||||
"openhab.host": {
|
||||
"type": [
|
||||
|
@ -188,7 +188,7 @@
|
|||
"default": 8080,
|
||||
"description": "Specifies the port for the openHAB preview."
|
||||
},
|
||||
"openhab.lspPort": {
|
||||
"openhab.remoteLspPort": {
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
|
@ -196,19 +196,12 @@
|
|||
"default": 5007,
|
||||
"description": "Specifies the port where openHAB is running its Language Server."
|
||||
},
|
||||
"openhab.lspEnabled": {
|
||||
"openhab.remoteLspEnabled": {
|
||||
"type": [
|
||||
"boolean"
|
||||
],
|
||||
"default": true,
|
||||
"description": "Enables communication with Language Server Protocol - installed in openHAB as 'misc-lsp' add-on"
|
||||
},
|
||||
"openhab.restCompletions": {
|
||||
"type": [
|
||||
"boolean"
|
||||
],
|
||||
"default": true,
|
||||
"description": "Activates item completions from REST API. (Disable this option, when you are facing performance problems on your openHAB environment.)"
|
||||
"description": "Enables communication with Language Server of openHAB instance."
|
||||
},
|
||||
"openhab.username": {
|
||||
"type": [
|
||||
|
@ -329,29 +322,24 @@
|
|||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "tsc -p ./",
|
||||
"compile": "tsc -watch -p ./",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
"compile": "tsc -b",
|
||||
"watch": "tsc -b -w",
|
||||
"postinstall": "cd client && npm install && cd ../serverJS && npm install && cd ..",
|
||||
"test": "cd client && npm test && cd ../serverJS && npm test && cd ..",
|
||||
"clean": "rm -rf node_modules && rm -rf **/node_modules && rm -rf **/out",
|
||||
"pruneProduction": "npm prune --production && cd client && npm prune --production && cd ../serverJS && npm prune --production && cd ..",
|
||||
"build": "npm run clean && npm install && npm run compile && npm run test && npm run pruneProduction",
|
||||
"package": "npm run build && vsce package",
|
||||
"publish": "npm run build && vsce publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^2.2.48",
|
||||
"@types/node": "^6.14.2",
|
||||
"mocha": "^2.3.3",
|
||||
"typescript": "^2.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/form-data": "^2.2.0",
|
||||
"@types/form-data": "^2.2.1",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/node": "^8.10.0",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/request-promise-native": "^1.0.15",
|
||||
"ascii-table": "0.0.9",
|
||||
"copy-paste": "^1.3.0",
|
||||
"lodash": "^4.17.4",
|
||||
"request": "^2.83.0",
|
||||
"jest": "^23.6.0",
|
||||
"request-promise-native": "^1.0.5",
|
||||
"underscore.string": "^3.3.5",
|
||||
"vscode": "^1.1.26",
|
||||
"vscode-languageclient": "^3.5.1"
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-env jest */
|
||||
jest.mock('../../src/DocumentValidation/DocumentValidator', () => {
|
||||
return {
|
||||
validateTextDocument: jest.fn((str) => 'MockedDiagnosticsFor' + str)
|
||||
}
|
||||
})
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-env jest */
|
||||
jest.mock('../../src/ItemCompletion/ItemCompletionProvider', () => {
|
||||
const itemCompletionProvider = jest.fn(() => {
|
||||
const instance = {
|
||||
start: jest.fn(() => Promise.resolve()),
|
||||
stop: jest.fn(),
|
||||
completionItems: {},
|
||||
restartIfConfigChanged: jest.fn(() => Promise.resolve())
|
||||
}
|
||||
|
||||
// required to spy on getter
|
||||
Object.defineProperty(instance, 'completionItems', {
|
||||
get: function () {
|
||||
// faked value to check if server returns correct value
|
||||
return [{
|
||||
label: 'Label',
|
||||
kind: 0,
|
||||
detail: 'Switch'
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
return instance
|
||||
})
|
||||
|
||||
return itemCompletionProvider
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint-env jest */
|
||||
jest.mock('../src/Server', () => {
|
||||
return jest.fn(() => {
|
||||
return {
|
||||
start: jest.fn()
|
||||
}
|
||||
})
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
/* eslint-env jest */
|
||||
const vscodeLanguageServer = jest.genMockFromModule('vscode-languageserver')
|
||||
|
||||
this.defaultConnection = {
|
||||
onInitialize: jest.fn(),
|
||||
onInitialized: jest.fn(),
|
||||
onExit: jest.fn(),
|
||||
onDidChangeConfiguration: jest.fn(),
|
||||
onDidChangeWatchedFiles: jest.fn(),
|
||||
onCompletion: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
workspace: {
|
||||
getConfiguration: jest.fn(() => {
|
||||
return Promise.resolve({ mock: 'config' })
|
||||
})
|
||||
},
|
||||
client: {
|
||||
register: jest.fn()
|
||||
},
|
||||
sendDiagnostics: jest.fn()
|
||||
}
|
||||
|
||||
this.defaultDocuments = {
|
||||
onDidSave: jest.fn(),
|
||||
onDidOpen: jest.fn(),
|
||||
onDidClose: jest.fn(),
|
||||
onDidChangeContent: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
syncKind: 'mockValue'
|
||||
}
|
||||
|
||||
vscodeLanguageServer.createConnection = jest.fn(() => this.connection || this.defaultConnection)
|
||||
vscodeLanguageServer.TextDocuments = jest.fn(() => this.documents || this.defaultDocuments)
|
||||
|
||||
vscodeLanguageServer.__setConnection = connObj => {
|
||||
this.connection = connObj
|
||||
}
|
||||
|
||||
vscodeLanguageServer.__getConnection = () => {
|
||||
return this.connection || this.defaultConnection
|
||||
}
|
||||
|
||||
vscodeLanguageServer.__setDocuments = docsObj => {
|
||||
this.documents = docsObj
|
||||
}
|
||||
|
||||
vscodeLanguageServer.__getDocuments = () => {
|
||||
return this.documents || this.defaultDocuments
|
||||
}
|
||||
|
||||
module.exports = vscodeLanguageServer
|
|
@ -0,0 +1,86 @@
|
|||
/* eslint-env jest */
|
||||
const ItemCompletionProvider = require('../../../src/ItemCompletion/ItemCompletionProvider')
|
||||
// const request = require('request')
|
||||
|
||||
jest.setTimeout(20000)
|
||||
|
||||
describe('Integration tests for item completion', () => {
|
||||
test('Create and start item completion', async () => {
|
||||
const icp = new ItemCompletionProvider()
|
||||
const err = await icp.start('demo.openhab.org', 8080)
|
||||
expect(err).toBeUndefined()
|
||||
expect(icp.isRunning).toBeTruthy()
|
||||
expect(icp.items).toBeDefined()
|
||||
expect(icp.items.size).toBeGreaterThan(0)
|
||||
|
||||
expect(icp.completionItems).toBeDefined()
|
||||
expect(icp.completionItems.length).toBeGreaterThan(0)
|
||||
// check one item if it looks like completion items from vscode
|
||||
expect(icp.completionItems[0]).toEqual({ detail: expect.anything(String), documentation: expect.anything(String), kind: 6, label: expect.anything(String) })
|
||||
icp.stop()
|
||||
})
|
||||
|
||||
test('Create and start item completion with localhost where no service is available', async () => {
|
||||
// eventsource timeout will fail test because default is 5s
|
||||
const icp = new ItemCompletionProvider()
|
||||
const err = await icp.start('localhost', 8080)
|
||||
expect(err.message).toEqual('connect ECONNREFUSED 127.0.0.1:8080')
|
||||
expect(icp.isRunning).toBeFalsy()
|
||||
icp.stop()
|
||||
})
|
||||
|
||||
test('Create and start item completion with wrong host', async () => {
|
||||
// eventsource timeout will fail test because default is 5s
|
||||
const icp = new ItemCompletionProvider()
|
||||
const err = await icp.start(123, 8080)
|
||||
expect(err.message).toEqual('getaddrinfo ENOTFOUND 123 123:8080')
|
||||
expect(icp.isRunning).toBeFalsy()
|
||||
icp.stop()
|
||||
})
|
||||
|
||||
test('Create and start item completion with wrong port', async () => {
|
||||
// eventsource timeout will fail test because default is 5s
|
||||
const icp = new ItemCompletionProvider()
|
||||
const err = await icp.start(123, 'abc')
|
||||
// seems to fallback to 80 if port is NaN
|
||||
expect(err.message).toEqual('getaddrinfo ENOTFOUND 123 123:80')
|
||||
expect(icp.isRunning).toBeFalsy()
|
||||
icp.stop()
|
||||
})
|
||||
|
||||
test('Restart service', async () => {
|
||||
const icp = new ItemCompletionProvider()
|
||||
let err = await icp.start('demo.openhab.org', 8080)
|
||||
expect(err).toBeUndefined()
|
||||
expect(icp.items).toBeDefined()
|
||||
expect(icp.items.size).toBeGreaterThan(0)
|
||||
expect(icp.isRunning).toBeTruthy()
|
||||
|
||||
err = await icp.restartIfConfigChanged('localhost', 1234)
|
||||
expect(err.message).toEqual('connect ECONNREFUSED 127.0.0.1:1234')
|
||||
expect(icp.items).toBeDefined()
|
||||
expect(icp.items.size).toBe(0)
|
||||
expect(icp.isRunning).toBeFalsy()
|
||||
|
||||
err = await icp.restartIfConfigChanged('demo.openhab.org', 8080)
|
||||
expect(err).toBeUndefined()
|
||||
expect(icp.items).toBeDefined()
|
||||
expect(icp.items.size).toBeGreaterThan(0)
|
||||
expect(icp.isRunning).toBeTruthy()
|
||||
|
||||
icp.stop()
|
||||
})
|
||||
|
||||
test('test events', async () => {
|
||||
const icp = new ItemCompletionProvider()
|
||||
let err = await icp.start('demo.openhab.org', 8080)
|
||||
expect(err).toBeUndefined()
|
||||
expect(icp.items).toBeDefined()
|
||||
expect(icp.items.size).toBeGreaterThan(0)
|
||||
expect(icp.isRunning).toBeTruthy()
|
||||
|
||||
// TODO send state changes, remove, update and create events to REST api and check if we get the results
|
||||
|
||||
icp.stop()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const Server = require('../../src/Server')
|
||||
|
||||
describe('Integration tests for LSP server', () => {
|
||||
test('Start LSP server', async () => {
|
||||
const LSPServer = require('../../src/LSPServer')
|
||||
expect(LSPServer.server).toBeDefined()
|
||||
expect(LSPServer.server).toBeInstanceOf(Server)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,47 @@
|
|||
/* eslint-env jest */
|
||||
const Server = require('../../src/Server')
|
||||
|
||||
describe('Integration tests for server', () => {
|
||||
test('Start server, initialize item completion and get completion items', async () => {
|
||||
const server = new Server()
|
||||
server.globalSettings = { host: 'demo.openhab.org', port: 8080 }
|
||||
|
||||
server.start()
|
||||
const err = await server.initializeItemCompletionProvider()
|
||||
expect(err).toBeUndefined()
|
||||
const completions = server.getCompletion()
|
||||
expect(completions).toBeDefined()
|
||||
expect(completions.length).toBeGreaterThan(0)
|
||||
server.exit()
|
||||
})
|
||||
|
||||
test('Start server, fail initialize item completion and get completion items', async () => {
|
||||
const server = new Server()
|
||||
server.globalSettings = { host: 'localhost', port: 8080 }
|
||||
|
||||
server.start()
|
||||
const err = await server.initializeItemCompletionProvider()
|
||||
expect(err.message).toEqual('connect ECONNREFUSED 127.0.0.1:8080')
|
||||
const completions = server.getCompletion()
|
||||
expect(completions).toBeDefined()
|
||||
expect(completions.length).toEqual(0)
|
||||
server.exit()
|
||||
})
|
||||
|
||||
test('Start server and validate documents', async () => {
|
||||
const server = new Server()
|
||||
server.globalSettings = { host: 'demo.openhab.org', port: 8080 }
|
||||
|
||||
server.start()
|
||||
|
||||
// as we do not have a client for testing here we mock the sendDiagnostics to check if we get the result
|
||||
server.connection.sendDiagnostics = jest.fn()
|
||||
server.validateDocument({ uri: 'testDocument.txt' })
|
||||
|
||||
// this test does not make so much sense yet, however when document validation is available we can test everything here
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledTimes(1)
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledWith({ diagnostics: [], uri: 'testDocument.txt' })
|
||||
|
||||
server.exit()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint-env jest */
|
||||
const { validateTextDocument } = require('../../../src/DocumentValidation/DocumentValidator')
|
||||
|
||||
describe('Tests for validation', () => {
|
||||
test('this does not do much yet', () => {
|
||||
expect(validateTextDocument({ uri: 'test.txt' })).toEqual({ uri: 'test.txt', diagnostics: [] })
|
||||
})
|
||||
})
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const Item = require('../../../src/ItemCompletion/Item')
|
||||
|
||||
describe('Tests for Item', () => {
|
||||
test('Create item and check if methods return expected values', () => {
|
||||
const restItem = {
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
}
|
||||
const item = new Item(restItem)
|
||||
|
||||
expect(item.item).toBe(restItem)
|
||||
|
||||
expect(item.name).toEqual('Weather_Chart')
|
||||
expect(item.type).toEqual('Group')
|
||||
expect(item.label).toBeUndefined()
|
||||
expect(item.category).toBeUndefined()
|
||||
expect(item.state).toEqual('')
|
||||
expect(item.link).toEqual('http://demo.openhab.org:8080/rest/items/Weather_Chart')
|
||||
expect(item.icon).toEqual('none.svg')
|
||||
expect(item.isGroup).toBeTruthy()
|
||||
expect(item.isRootItem).toBeTruthy()
|
||||
expect(item.tags).toEqual([])
|
||||
expect(item.groupNames).toEqual([])
|
||||
expect(item.members).toEqual([])
|
||||
})
|
||||
|
||||
test('Create item and check special cases', () => {
|
||||
const restItem = {
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'UNDEF',
|
||||
editable: false,
|
||||
type: 'Switch',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: ['group5'],
|
||||
category: 'window'
|
||||
}
|
||||
const item = new Item(restItem)
|
||||
|
||||
expect(item.item).toBe(restItem)
|
||||
|
||||
expect(item.state).toEqual('')
|
||||
expect(item.icon).toEqual('window.svg')
|
||||
expect(item.isRootItem).toBeFalsy()
|
||||
expect(item.isGroup).toBeFalsy()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,528 @@
|
|||
/* eslint-env jest */
|
||||
require('../../../__mocks__/vscode-languageserver')
|
||||
|
||||
jest.mock('eventsource', () => {
|
||||
const es = jest.fn(host => {
|
||||
return {
|
||||
addEventListener: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
return es
|
||||
})
|
||||
|
||||
jest.mock('request', () => {
|
||||
const request = jest.fn((host, json, cb) => {
|
||||
cb(this.error, undefined, this.items)
|
||||
})
|
||||
|
||||
request.__setError = err => {
|
||||
this.error = err
|
||||
}
|
||||
|
||||
request.__setItems = items => {
|
||||
this.items = items
|
||||
}
|
||||
|
||||
return request
|
||||
})
|
||||
|
||||
const ItemCompletionProvider = require('../../../src/ItemCompletion/ItemCompletionProvider')
|
||||
const request = require('request')
|
||||
const Item = require('../../../src/ItemCompletion/Item')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
request.__setItems(undefined)
|
||||
request.__setError(undefined)
|
||||
})
|
||||
|
||||
describe('Tests for item completion', () => {
|
||||
test('.getItemsFromRestApi where request has error', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
|
||||
request.__setError(new Error('Error'))
|
||||
|
||||
return completions
|
||||
.getItemsFromRestApi('localhost', 1234)
|
||||
.then(() => {
|
||||
// Should never get here
|
||||
expect(1).toBe(2)
|
||||
})
|
||||
.catch(error => {
|
||||
expect(request).toHaveBeenCalledTimes(1)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/rest/items/',
|
||||
{ json: true },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(error).toEqual(new Error('Error'))
|
||||
})
|
||||
})
|
||||
|
||||
test('.getItemsFromRestApi where request does not get valid items', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
|
||||
// invalid response
|
||||
request.__setItems({ items: false })
|
||||
|
||||
return completions
|
||||
.getItemsFromRestApi('localhost', 1234)
|
||||
.then(() => {
|
||||
// should never get here
|
||||
expect(1).toBe(2)
|
||||
})
|
||||
.catch(err => {
|
||||
expect(request).toHaveBeenCalledTimes(1)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/rest/items/',
|
||||
{ json: true },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(err).toEqual(new Error('Could not get valid data from REST API'))
|
||||
})
|
||||
})
|
||||
|
||||
test('.getItemsFromRestApi where request gets empty items array', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
completions.items = new Map()
|
||||
|
||||
// response with empty array (no items on openhab)
|
||||
request.__setItems([])
|
||||
|
||||
return completions.getItemsFromRestApi('localhost', 1234).then(() => {
|
||||
expect(request).toHaveBeenCalledTimes(1)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/rest/items/',
|
||||
{ json: true },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(completions.items.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('.getItemsFromRestApi where request gets valid items array', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
completions.items = new Map()
|
||||
|
||||
request.__setItems([
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
},
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/FF_Bathroom',
|
||||
state: 'NULL',
|
||||
editable: true,
|
||||
type: 'Group',
|
||||
name: 'FF_Bathroom',
|
||||
label: 'Bathroom',
|
||||
category: 'bath',
|
||||
tags: ['Bathroom'],
|
||||
groupNames: ['Home', 'FF']
|
||||
},
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Status',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Status',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
}
|
||||
])
|
||||
|
||||
return completions.getItemsFromRestApi('localhost', 1234).then(() => {
|
||||
expect(request).toHaveBeenCalledTimes(1)
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/rest/items/',
|
||||
{ json: true },
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(completions.items.size).toBe(3)
|
||||
}).catch(() => {
|
||||
// should never get here
|
||||
expect(1).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
test('.stop with running eventsource', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
completions.items = {}
|
||||
completions.status = 'started'
|
||||
completions.es = {
|
||||
close: jest.fn()
|
||||
}
|
||||
|
||||
completions.stop()
|
||||
expect(completions.items).toBeUndefined()
|
||||
expect(completions.status).toEqual('stopped')
|
||||
expect(completions.es.close).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('.stop without eventsource', () => {
|
||||
const completions = new ItemCompletionProvider()
|
||||
completions.items = {}
|
||||
completions.status = 'started'
|
||||
|
||||
completions.stop()
|
||||
expect(completions.items).toBeUndefined()
|
||||
expect(completions.status).toEqual('stopped')
|
||||
})
|
||||
|
||||
test('.restartIfConfigChanged', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
completion.host = 'localhost'
|
||||
completion.port = 1234
|
||||
|
||||
completion.stop = jest.fn()
|
||||
completion.start = jest.fn()
|
||||
|
||||
// nothing changes
|
||||
let err = await completion.restartIfConfigChanged('localhost', 1234)
|
||||
expect(err).toBeUndefined()
|
||||
expect(completion.stop).toHaveBeenCalledTimes(0)
|
||||
expect(completion.start).toHaveBeenCalledTimes(0)
|
||||
|
||||
// port changes
|
||||
err = await completion.restartIfConfigChanged('localhost', 12345)
|
||||
expect(err).toBeUndefined()
|
||||
expect(completion.stop).toHaveBeenCalledTimes(1)
|
||||
expect(completion.start).toHaveBeenCalledTimes(1)
|
||||
expect(completion.start).toHaveBeenCalledWith('localhost', 12345)
|
||||
|
||||
// host changes
|
||||
err = await completion.restartIfConfigChanged('localhost1', 1234)
|
||||
expect(err).toBeUndefined()
|
||||
expect(completion.stop).toHaveBeenCalledTimes(2) // this increments as we do not clear the mock inbetween
|
||||
expect(completion.start).toHaveBeenCalledTimes(2)
|
||||
expect(completion.start).toHaveBeenCalledWith('localhost1', 1234)
|
||||
|
||||
// both changes
|
||||
err = await completion.restartIfConfigChanged('text', 0)
|
||||
expect(err).toBeUndefined()
|
||||
expect(completion.stop).toHaveBeenCalledTimes(3) // this increments as we do not clear the mock inbetween
|
||||
expect(completion.start).toHaveBeenCalledTimes(3)
|
||||
expect(completion.start).toHaveBeenCalledWith('text', 0)
|
||||
})
|
||||
|
||||
test('.completionItems returns empty array if no items map is present', () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
expect(completion.completionItems).toEqual([])
|
||||
})
|
||||
|
||||
test('.completionItems returns array if items are present', () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
completion.items = new Map()
|
||||
completion.items.set(
|
||||
'Weather_Chart',
|
||||
new Item({
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
})
|
||||
)
|
||||
|
||||
expect(completion.completionItems).toEqual([
|
||||
{ detail: 'Group', documentation: '', kind: 6, label: 'Weather_Chart' }
|
||||
])
|
||||
})
|
||||
|
||||
test('.getDocumentation', () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
// no values available
|
||||
expect(completion.getDocumentation({ tags: [], groupNames: [] })).toEqual(
|
||||
''
|
||||
)
|
||||
|
||||
// label only
|
||||
expect(
|
||||
completion.getDocumentation({ label: 'label', tags: [], groupNames: [] })
|
||||
).toEqual('label')
|
||||
|
||||
// state only
|
||||
expect(
|
||||
completion.getDocumentation({ state: 'ON', tags: [], groupNames: [] })
|
||||
).toEqual('(ON)')
|
||||
|
||||
// tag only
|
||||
expect(
|
||||
completion.getDocumentation({ tags: ['LIGHTING'], groupNames: [] })
|
||||
).toEqual('Tags: LIGHTING')
|
||||
|
||||
// group only
|
||||
expect(
|
||||
completion.getDocumentation({ tags: [], groupNames: ['group'] })
|
||||
).toEqual('Groups: group')
|
||||
|
||||
// mixed
|
||||
expect(
|
||||
completion.getDocumentation({
|
||||
label: 'label',
|
||||
state: 'ON',
|
||||
tags: ['LIGHTING', 'SWITCH'],
|
||||
groupNames: ['group', 'light']
|
||||
})
|
||||
).toEqual('label (ON)\nTags: LIGHTING, SWITCH\nGroups: group, light')
|
||||
})
|
||||
|
||||
test('.isRunning', () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
completion.es = { CONNECTING: true }
|
||||
completion.status = undefined
|
||||
expect(completion.isRunning).toBeTruthy()
|
||||
|
||||
completion.es = { OPEN: true }
|
||||
completion.status = undefined
|
||||
expect(completion.isRunning).toBeTruthy()
|
||||
|
||||
completion.es = {}
|
||||
completion.status = 'connecting'
|
||||
expect(completion.isRunning).toBeTruthy()
|
||||
|
||||
completion.es = { CLOSED: true }
|
||||
completion.status = undefined
|
||||
expect(completion.isRunning).toBeFalsy()
|
||||
|
||||
completion.es = {}
|
||||
completion.status = 'stopped'
|
||||
expect(completion.isRunning).toBeFalsy()
|
||||
})
|
||||
|
||||
test('.event', () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
completion.items = new Map()
|
||||
|
||||
// item state event
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/MC_Wohnzimmer_Volume/state","payload":"{\\"type\\":\\"Decimal\\",\\"value\\":\\"93\\"}","type":"ItemStateEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
// if no item exists for item state event nothing should happen
|
||||
expect(completion.items.size).toBe(0)
|
||||
|
||||
// item added
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/added","payload":"{\\"type\\":\\"String\\",\\"name\\":\\"TestItem\\",\\"label\\":\\"lala\\",\\"tags\\":[],\\"groupNames\\":[]}","type":"ItemAddedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
expect(completion.items.size).toBe(1)
|
||||
expect(completion.items.get('TestItem')).toEqual({
|
||||
item: {
|
||||
groupNames: [],
|
||||
label: 'lala',
|
||||
name: 'TestItem',
|
||||
tags: [],
|
||||
type: 'String'
|
||||
}
|
||||
})
|
||||
|
||||
// item updated
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/updated","payload":"[{\\"type\\":\\"String\\",\\"name\\":\\"TestItem\\",\\"label\\":\\"l1ala\\",\\"tags\\":[],\\"groupNames\\":[]},{\\"type\\":\\"String\\",\\"name\\":\\"TestItem\\",\\"label\\":\\"lala\\",\\"tags\\":[],\\"groupNames\\":[]}]","type":"ItemUpdatedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
expect(completion.items.size).toBe(1)
|
||||
expect(completion.items.get('TestItem')).toEqual({
|
||||
item: {
|
||||
groupNames: [],
|
||||
label: 'l1ala',
|
||||
name: 'TestItem',
|
||||
tags: [],
|
||||
type: 'String'
|
||||
}
|
||||
})
|
||||
|
||||
// item state event
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/state","payload":"{\\"type\\":\\"String\\",\\"value\\":\\"BlaBla\\"}","type":"ItemStateEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
// update item
|
||||
expect(completion.items.size).toBe(1)
|
||||
expect(completion.items.get('TestItem')).toEqual({
|
||||
item: {
|
||||
groupNames: [],
|
||||
label: 'l1ala',
|
||||
name: 'TestItem',
|
||||
tags: [],
|
||||
type: 'String',
|
||||
state: 'BlaBla'
|
||||
}
|
||||
})
|
||||
|
||||
// item state changed event does not change data
|
||||
const itembefore = completion.items.get('TestItem')
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/statechanged","payload":"{\\"type\\":\\"String\\",\\"value\\":\\"lala\\",\\"oldType\\":\\"String\\",\\"oldValue\\":\\"blabla\\"}","type":"ItemStateChangedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
const itemAfter = completion.items.get('TestItem')
|
||||
expect(itembefore).toEqual(itemAfter)
|
||||
|
||||
// item removed
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/removed","payload":"{\\"type\\":\\"String\\",\\"name\\":\\"TestItem\\",\\"label\\":\\"lala\\",\\"tags\\":[],\\"groupNames\\":[]}","type":"ItemRemovedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
expect(completion.items.size).toBe(0)
|
||||
|
||||
// any other
|
||||
// item state changed event
|
||||
completion.event({
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/statechanged","payload":"{\\"type\\":\\"String\\",\\"value\\":\\"lala\\",\\"oldType\\":\\"String\\",\\"oldValue\\":\\"blabla\\"}","type":"ItemStateChangedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
})
|
||||
expect(completion.items.size).toBe(0)
|
||||
})
|
||||
|
||||
test('.start() is sucessful', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
request.__setItems([
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
},
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/FF_Bathroom',
|
||||
state: 'NULL',
|
||||
editable: true,
|
||||
type: 'Group',
|
||||
name: 'FF_Bathroom',
|
||||
label: 'Bathroom',
|
||||
category: 'bath',
|
||||
tags: ['Bathroom'],
|
||||
groupNames: ['Home', 'FF']
|
||||
},
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Status',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Status',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
}
|
||||
])
|
||||
|
||||
const res = await completion.start('localhost', 1234)
|
||||
expect(res).toBeUndefined()
|
||||
expect(completion.status).toEqual('connecting')
|
||||
expect(completion.es.addEventListener).toHaveBeenCalledTimes(1)
|
||||
expect(completion.items.size).toBe(3)
|
||||
})
|
||||
|
||||
test('.start() is sucessful, empty item array', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
request.__setItems([])
|
||||
|
||||
const res = await completion.start('localhost', 1234)
|
||||
expect(res).toBeUndefined()
|
||||
expect(completion.status).toEqual('connecting')
|
||||
expect(completion.es.addEventListener).toHaveBeenCalledTimes(1)
|
||||
expect(completion.items.size).toBe(0)
|
||||
})
|
||||
|
||||
test('.start() is not sucessful, no valid item array', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
request.__setItems()
|
||||
|
||||
const res = await completion.start('localhost', 1234)
|
||||
expect(res).toEqual(new Error('Could not get valid data from REST API'))
|
||||
expect(completion.status).toEqual('stopped')
|
||||
expect(completion.es).toBeUndefined()
|
||||
expect(completion.items.size).toBe(0)
|
||||
})
|
||||
|
||||
test('.start() is not sucessful, error in request', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
request.__setError(new Error('mocked error'))
|
||||
|
||||
const res = await completion.start('localhost', 1234)
|
||||
expect(res).toEqual(new Error('mocked error'))
|
||||
expect(completion.status).toEqual('stopped')
|
||||
expect(completion.es).toBeUndefined()
|
||||
expect(completion.items.size).toBe(0)
|
||||
})
|
||||
|
||||
test('.event() is called on event', async () => {
|
||||
const completion = new ItemCompletionProvider()
|
||||
|
||||
request.__setItems([
|
||||
{
|
||||
members: [],
|
||||
link: 'http://demo.openhab.org:8080/rest/items/Weather_Chart',
|
||||
state: 'NULL',
|
||||
editable: false,
|
||||
type: 'Group',
|
||||
name: 'Weather_Chart',
|
||||
tags: [],
|
||||
groupNames: []
|
||||
}])
|
||||
|
||||
completion.event = jest.fn()
|
||||
const res = await completion.start('localhost', 1234)
|
||||
expect(res).toBeUndefined()
|
||||
const arg1 = completion.es.addEventListener.mock.calls[0][0]
|
||||
const callback = completion.es.addEventListener.mock.calls[0][1]
|
||||
expect(arg1).toEqual('message')
|
||||
const event = {
|
||||
type: 'message',
|
||||
data:
|
||||
'{"topic":"smarthome/items/TestItem/statechanged","payload":"{\\"type\\":\\"String\\",\\"value\\":\\"lala\\",\\"oldType\\":\\"String\\",\\"oldValue\\":\\"blabla\\"}","type":"ItemStateChangedEvent"}',
|
||||
lastEventId: '',
|
||||
origin: 'http://openhabianpi.local:8080'
|
||||
}
|
||||
callback(event)
|
||||
expect(completion.event).toHaveBeenCalledWith(event)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env jest */
|
||||
require('../../__mocks__/Server')
|
||||
|
||||
const lspServer = require('../../src/LSPServer')
|
||||
|
||||
describe('LSP Server tests', () => {
|
||||
test('server is started', () => {
|
||||
expect(lspServer.server.start).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,187 @@
|
|||
/* eslint-env jest */
|
||||
require('../../__mocks__/ItemCompletion/ItemCompletionProvider')
|
||||
require('../../__mocks__/DocumentValidation/DocumentValidator')
|
||||
|
||||
const Server = require('../../src/Server')
|
||||
const vscodeLanguageserver = require('vscode-languageserver')
|
||||
const { validateTextDocument } = require('../../src/DocumentValidation/DocumentValidator')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Server Tests', () => {
|
||||
test('.start() initializes vscode language server', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
expect(vscodeLanguageserver.createConnection).toHaveBeenCalledTimes(1)
|
||||
|
||||
// connection listeners are registered
|
||||
const connection = vscodeLanguageserver.__getConnection()
|
||||
expect(connection.onInitialize).toHaveBeenCalledTimes(1)
|
||||
expect(connection.onInitialized).toHaveBeenCalledTimes(1)
|
||||
expect(connection.onExit).toHaveBeenCalledTimes(1)
|
||||
expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1)
|
||||
// expect(connection.onDidChangeWatchedFiles).toHaveBeenCalledTimes(1)
|
||||
expect(connection.onCompletion).toHaveBeenCalledTimes(1)
|
||||
expect(connection.listen).toHaveBeenCalledTimes(1)
|
||||
|
||||
// text documents listeners are registered
|
||||
const documents = vscodeLanguageserver.__getDocuments()
|
||||
// expect(documents.onDidSave).toHaveBeenCalledTimes(1)
|
||||
expect(documents.onDidOpen).toHaveBeenCalledTimes(1)
|
||||
// expect(documents.onDidClose).toHaveBeenCalledTimes(1)
|
||||
expect(documents.onDidChangeContent).toHaveBeenCalledTimes(1)
|
||||
expect(documents.listen).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(documents.listen).toHaveBeenCalledWith(connection)
|
||||
})
|
||||
|
||||
test('.initializeItemCompletionProvider initializes ItemCompletionProvider with params from settings', () => {
|
||||
const server = new Server()
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
server.initializeItemCompletionProvider()
|
||||
expect(server.itemsCompletionProvider.start).toHaveBeenCalledTimes(1)
|
||||
expect(server.itemsCompletionProvider.start).toHaveBeenCalledWith('localhost', 123)
|
||||
})
|
||||
|
||||
test('.getCompletion calls method from ItemCompletionProvider and returns value', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
server.initializeItemCompletionProvider()
|
||||
|
||||
const completionItems = jest.spyOn(server.itemsCompletionProvider, 'completionItems', 'get')
|
||||
|
||||
const getCompletion = server.connection.onCompletion.mock.calls[0][0]
|
||||
|
||||
const completion = getCompletion()
|
||||
expect(completionItems).toHaveBeenCalledTimes(1)
|
||||
expect(completion).toEqual([{ detail: 'Switch', kind: 0, label: 'Label' }])
|
||||
})
|
||||
|
||||
test('.exit should also close ItemCompletionProvider', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
|
||||
const exit = server.connection.onExit.mock.calls[0][0]
|
||||
|
||||
// without ItemCompletionProvider exit should work
|
||||
exit()
|
||||
expect(server.itemsCompletionProvider).toBeUndefined()
|
||||
|
||||
server.initializeItemCompletionProvider()
|
||||
exit()
|
||||
expect(server.itemsCompletionProvider.stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('.getGlobalConfig gets config from client', async () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
const res = await server.getGlobalConfig()
|
||||
expect(res).toEqual({ mock: 'config' })
|
||||
})
|
||||
|
||||
test('.initialize returns capabilities object', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
// get callback method that was passed in .start()
|
||||
// this.connection.onInitialize(() => this.initialize(arguments))
|
||||
// with the mock.calls[0][0] we get () => this.initialize(arguments)
|
||||
// we need to pass this in an anonymous function to keep the 'this' context
|
||||
const initialize = server.connection.onInitialize.mock.calls[0][0]
|
||||
const res = initialize({ capablities: 'mockedCapability' })
|
||||
expect(res).toEqual({
|
||||
capabilities: { completionProvider: { resolveProvider: false }, textDocumentSync: 'mockValue' }
|
||||
})
|
||||
})
|
||||
|
||||
test('.initialized registers more listeners and gets settings and starts item completion listener', async () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
const initialized = server.connection.onInitialized.mock.calls[0][0]
|
||||
await initialized()
|
||||
expect(server.connection.client.register).toHaveBeenCalledTimes(1)
|
||||
expect(server.itemsCompletionProvider.start).toHaveBeenCalledTimes(1)
|
||||
expect(server.globalSettings).toEqual({ mock: 'config' })
|
||||
})
|
||||
|
||||
test('.configurationChanged event with no fitting values are ignored', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
|
||||
const configurationChanged = server.connection.onDidChangeConfiguration.mock.calls[0][0]
|
||||
|
||||
configurationChanged({ host: 'remotehost', port: 456 })
|
||||
expect(server.globalSettings).toEqual({ host: 'localhost', port: 123 })
|
||||
|
||||
configurationChanged({ settings: { host: 'remotehost', port: 456 } })
|
||||
expect(server.globalSettings).toEqual({ host: 'localhost', port: 123 })
|
||||
|
||||
configurationChanged({ settings: { openhab: undefined } })
|
||||
expect(server.globalSettings).toEqual({ host: 'localhost', port: 123 })
|
||||
})
|
||||
|
||||
test('.configurationChanged called with changes will update config', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
|
||||
const configurationChanged = server.connection.onDidChangeConfiguration.mock.calls[0][0]
|
||||
|
||||
configurationChanged({ settings: { openhab: {} } })
|
||||
expect(server.globalSettings).toEqual({})
|
||||
|
||||
configurationChanged({ settings: { openhab: { host: 'remotehost', port: 456 } } })
|
||||
expect(server.globalSettings).toEqual({ host: 'remotehost', port: 456 })
|
||||
})
|
||||
|
||||
test('.configurationChanged called with changes will update config', async () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
const initialized = server.connection.onInitialized.mock.calls[0][0]
|
||||
await initialized()
|
||||
|
||||
server.globalSettings = { host: 'localhost', port: 123 }
|
||||
|
||||
server.configurationChanged({ settings: { openhab: { host: 'remotehost', port: 456 } } })
|
||||
expect(server.globalSettings).toEqual({ host: 'remotehost', port: 456 })
|
||||
expect(server.itemsCompletionProvider.restartIfConfigChanged).toHaveBeenCalledTimes(1)
|
||||
expect(server.itemsCompletionProvider.restartIfConfigChanged).toHaveBeenCalledWith('remotehost', 456)
|
||||
})
|
||||
|
||||
test('.documentOpened', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
server.globalSettings = { settings: 'mocked' }
|
||||
|
||||
const documentOpened = server.documents.onDidOpen.mock.calls[0][0]
|
||||
|
||||
documentOpened({ document: 'MockedDocument' })
|
||||
expect(validateTextDocument).toHaveBeenCalledTimes(1)
|
||||
expect(validateTextDocument).toHaveBeenCalledWith('MockedDocument', { settings: 'mocked' })
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledTimes(1)
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledWith('MockedDiagnosticsForMockedDocument')
|
||||
})
|
||||
|
||||
test('.documentChanged', () => {
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
server.globalSettings = { settings: 'mocked' }
|
||||
|
||||
const documentChanged = server.documents.onDidChangeContent.mock.calls[0][0]
|
||||
|
||||
documentChanged({ document: 'MockedDocumentChanged' })
|
||||
expect(validateTextDocument).toHaveBeenCalledTimes(1)
|
||||
expect(validateTextDocument).toHaveBeenCalledWith('MockedDocumentChanged', { settings: 'mocked' })
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledTimes(1)
|
||||
expect(server.connection.sendDiagnostics).toHaveBeenCalledWith('MockedDiagnosticsForMockedDocumentChanged')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 50,
|
||||
"functions": 50,
|
||||
"lines": 50,
|
||||
"statements": 50
|
||||
}
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!__mocks__/**",
|
||||
"!__tests__/**"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/integration/**/*.js"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 90,
|
||||
"functions": 100,
|
||||
"lines": 90,
|
||||
"statements": 90
|
||||
}
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!__mocks__/**",
|
||||
"!__tests__/**"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/unit/**/*.js"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "openhab-vscode-languageserver",
|
||||
"displayName": "openHAB VSCode Language Server for Viasual Studio Code",
|
||||
"description": "Provides completions and validation of openhab files.",
|
||||
"version": "0.1.0",
|
||||
"publisher": "openhab",
|
||||
"icon": "../openhab.png",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openhab/openhab-vscode.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageserver": "^5.2.1",
|
||||
"eventsource": "^1.0.7",
|
||||
"lodash": "^4.17.11",
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^23.6.0",
|
||||
"standard": "^12.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test-unit": "jest --coverage --detectOpenHandles --config=jest-unit.json",
|
||||
"test-integration": "jest --coverage --detectOpenHandles --config=jest-integration.json",
|
||||
"test": "npm run test-unit && npm run test-integration"
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
||||
"jest"
|
||||
],
|
||||
"globals": [
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* WIP this does not do anything yet
|
||||
* @param textDocument
|
||||
* @param settings
|
||||
* @author Samuel Brucksch
|
||||
*/
|
||||
const validateTextDocument = (textDocument, settings) => {
|
||||
// TODO think about reasonable validations
|
||||
let diagnostics = []
|
||||
// Send the computed diagnostics to VSCode.
|
||||
return { uri: textDocument.uri, diagnostics }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateTextDocument
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
'use strict'
|
||||
const _ = require('lodash')
|
||||
|
||||
class Item {
|
||||
constructor (item) {
|
||||
this.item = item
|
||||
}
|
||||
/**
|
||||
* The Item name is the unique identified of the Item.
|
||||
* The name should only consist of letters, numbers and the underscore character.
|
||||
* Spaces and special characters cannot be used.
|
||||
* e.g. 'Kitchen_Temperature'
|
||||
*/
|
||||
get name () {
|
||||
return this.item.name
|
||||
}
|
||||
/**
|
||||
* The Item type defines which kind of state can be stored in that Item and which commands can be sent to it.
|
||||
* Each Item type has been optimized for certain components in your smart home.
|
||||
* This optimization is reflected in the data types, and command types.
|
||||
*
|
||||
* Color|Contact|DateTime|Dimmer|Group|Number|Player|Rollershutter|String|Switch
|
||||
*/
|
||||
get type () {
|
||||
return this.item.type
|
||||
}
|
||||
set type (v) {
|
||||
this.item.type = v
|
||||
}
|
||||
/**
|
||||
* The label text has two purposes.
|
||||
* First, this text is used to display a description of the specific Item (for example, in the Sitemap).
|
||||
* Secondly, the label also includes the value displaying definition for the Item’s state.
|
||||
* e.g. "Kitchen thermometer"
|
||||
*/
|
||||
get label () {
|
||||
return this.item.label
|
||||
}
|
||||
/**
|
||||
* Item's category. Used for icons
|
||||
*/
|
||||
get category () {
|
||||
return this.item.category
|
||||
}
|
||||
set state (v) {
|
||||
this.item.state = v
|
||||
}
|
||||
/**
|
||||
* The state part of the Item definition determines the Item value presentation,
|
||||
* e.g., regarding formatting, decimal places, unit display and more.
|
||||
* The state definition is part of the Item Label definition and contained inside square brackets.
|
||||
* e.g. 'OFF' or '22'
|
||||
*/
|
||||
get state () {
|
||||
const nullType = ['NULL', 'UNDEF']
|
||||
return !_.includes(nullType, this.item.state) ? this.item.state : ''
|
||||
}
|
||||
/**
|
||||
* Absolute path to the Item in the REST API
|
||||
* e.g. 'http://home:8080/rest/items/Ground_Floor'
|
||||
*/
|
||||
get link () {
|
||||
return this.item.link.toString()
|
||||
}
|
||||
/**
|
||||
* Relative path to item's icon
|
||||
* e.g. '/icon/kitchen?format=svg'
|
||||
*/
|
||||
get icon () {
|
||||
let icon = this.item.category || 'none'
|
||||
return icon + '.svg'
|
||||
// return '/icon/' + icon + '?format=svg'
|
||||
}
|
||||
/**
|
||||
* True if type of the item is equal to 'Group'.
|
||||
*
|
||||
* The Group is a special Item Type.
|
||||
* It is used to define a category or collection in which you can nest/collect other Items or other Groups.
|
||||
* Groups are supported in Sitemaps, Automation Rules and other areas of openHAB.
|
||||
*/
|
||||
get isGroup () {
|
||||
return this.item.type === 'Group'
|
||||
}
|
||||
/**
|
||||
* True if the item doesn't belong to any group
|
||||
*/
|
||||
get isRootItem () {
|
||||
return this.item.groupNames && this.item.groupNames.length === 0
|
||||
}
|
||||
/**
|
||||
* Tags are used by some I/O add-ons.
|
||||
* Tags are only of interest if an add-on or integration README explicitly discusses their usage.
|
||||
*/
|
||||
get tags () {
|
||||
return this.item.tags
|
||||
}
|
||||
/**
|
||||
* Returns an Array of Groups the item belongs to.
|
||||
*/
|
||||
get groupNames () {
|
||||
return this.item.groupNames
|
||||
}
|
||||
/**
|
||||
* True if type of the item is equal to 'Group'
|
||||
*/
|
||||
get members () {
|
||||
return this.isGroup && this.item.members
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Item
|
|
@ -0,0 +1,175 @@
|
|||
'use strict'
|
||||
const vscodeLanguageserver = require('vscode-languageserver')
|
||||
const Eventsource = require('eventsource')
|
||||
const request = require('request')
|
||||
const Item = require('./Item')
|
||||
const _ = require('lodash')
|
||||
/**
|
||||
* Provides completion items from REST API from openhab instance.
|
||||
*
|
||||
* Currently only works with http.
|
||||
* Example host: http://openhab:8080/rest/items
|
||||
*
|
||||
* Completion items are cached and updated with SSE
|
||||
* http://openhab:8080/rest/events
|
||||
*
|
||||
* @author Samuel Brucksch
|
||||
*
|
||||
*/
|
||||
class ItemCompletionProvider {
|
||||
/**
|
||||
* Start item completion
|
||||
*
|
||||
* @param host REST API host
|
||||
* @param port Port to access REST API
|
||||
*/
|
||||
async start (host, port) {
|
||||
this.items = new Map()
|
||||
this.status = 'connecting'
|
||||
return this.getItemsFromRestApi(host, port)
|
||||
.then(() => {
|
||||
if (this.status !== 'stopped') {
|
||||
this.es = new Eventsource(
|
||||
`http://${host}:${port}/rest/events?topics=smarthome/items`
|
||||
)
|
||||
this.es.addEventListener('message', (...params) =>
|
||||
this.event(...params)
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// TODO where to correctly log this?
|
||||
console.log(error)
|
||||
this.status = 'stopped'
|
||||
this.es = undefined
|
||||
return error
|
||||
})
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this.es) {
|
||||
this.es.close()
|
||||
}
|
||||
this.status = 'stopped'
|
||||
this.items = undefined
|
||||
}
|
||||
|
||||
event (eventPayload) {
|
||||
const event = JSON.parse(eventPayload.data)
|
||||
event.payload = JSON.parse(event.payload)
|
||||
const itemName = event.topic.split('/')[2]
|
||||
let item
|
||||
switch (event.type) {
|
||||
case 'ItemStateEvent':
|
||||
// called when openhab reaceives an item state. There is also ItemStateChangedEvent, that only notifies you about changes
|
||||
// however the ItemStateChangedEvent is more or less the same as the ItemStateEvent for the change so we do not need to read both
|
||||
item = this.items.get(itemName)
|
||||
if (item) {
|
||||
// add new values to item
|
||||
item.state = event.payload.value
|
||||
item.type = event.payload.type
|
||||
this.items.set(itemName, item)
|
||||
}
|
||||
break
|
||||
case 'ItemUpdatedEvent':
|
||||
// update events use an array with 2 elements: array[0] = new, array[1] = old
|
||||
// we do not need to compare old and new name as renaming items causes a remove and added event
|
||||
// all changes are already in array[0] so we can just overwrite the old item with the new one
|
||||
item = new Item(event.payload[0])
|
||||
this.items.set(item.name, item)
|
||||
break
|
||||
case 'ItemAddedEvent':
|
||||
item = new Item(event.payload)
|
||||
this.items.set(item.name, item)
|
||||
break
|
||||
case 'ItemRemovedEvent':
|
||||
item = event.payload
|
||||
this.items.delete(item.name)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts item completion if host/port changed
|
||||
*
|
||||
* @param host REST API host
|
||||
* @param port Port to access REST API
|
||||
*/
|
||||
async restartIfConfigChanged (host, port) {
|
||||
if (host !== this.host || port !== this.port) {
|
||||
this.stop()
|
||||
const err = await this.start(host, port)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of CompletionItems
|
||||
*/
|
||||
get completionItems () {
|
||||
if (this.items) {
|
||||
return Array.from(this.items.values()).map(item => {
|
||||
return {
|
||||
label: item.name,
|
||||
kind: vscodeLanguageserver.CompletionItemKind.Variable,
|
||||
detail: item.type,
|
||||
documentation: this.getDocumentation(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
// return empty erray if no map is available
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if ItemCompletionProvider is already running
|
||||
*/
|
||||
get isRunning () {
|
||||
return (this.es && (this.es.CONNECTING || this.es.OPEN)) || this.status === 'connecting'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a documentation string for the IntelliSense auto-completion
|
||||
* Contains Item's label, state, tags and group names.
|
||||
* @param item openHAB Item
|
||||
*/
|
||||
getDocumentation (item) {
|
||||
const label = item.label ? item.label + ' ' : ''
|
||||
const state = item.state ? '(' + item.state + ')' : ''
|
||||
const tags = item.tags.length && 'Tags: ' + item.tags.join(', ')
|
||||
const groupNames =
|
||||
item.groupNames.length && 'Groups: ' + item.groupNames.join(', ')
|
||||
const documentation = [(label + state).trim(), tags, groupNames]
|
||||
return _.compact(documentation).join('\n')
|
||||
}
|
||||
|
||||
getItemsFromRestApi (host, port) {
|
||||
this.host = host
|
||||
this.port = port
|
||||
return new Promise((resolve, reject) => {
|
||||
request(
|
||||
`http://${host}:${port}/rest/items/`,
|
||||
{ json: true },
|
||||
(err, res, items) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(items)) {
|
||||
items.map(item => {
|
||||
this.items.set(item.name, new Item(item))
|
||||
})
|
||||
return resolve()
|
||||
}
|
||||
|
||||
reject(new Error('Could not get valid data from REST API'))
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ItemCompletionProvider
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @author Samuel Brucksch
|
||||
*/
|
||||
const Server = require('./Server')
|
||||
|
||||
const server = new Server()
|
||||
server.start()
|
||||
|
||||
module.exports = {
|
||||
server
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
'use strict'
|
||||
|
||||
const {
|
||||
TextDocuments,
|
||||
createConnection,
|
||||
ProposedFeatures,
|
||||
DidChangeConfigurationNotification
|
||||
} = require('vscode-languageserver')
|
||||
|
||||
const { validateTextDocument } = require('./DocumentValidation/DocumentValidator')
|
||||
|
||||
const ItemCompletionProvider = require('./ItemCompletion/ItemCompletionProvider')
|
||||
|
||||
/**
|
||||
* Actual LSP server implementation. Client requires a script that starts the server so we can not give it a class diretly.
|
||||
* @author Samuel Brucksch
|
||||
*/
|
||||
class Server {
|
||||
start () {
|
||||
this.connection = createConnection(ProposedFeatures.all)
|
||||
|
||||
// add handlers to connection
|
||||
this.connection.onInitialize(this.initialize.bind(this))
|
||||
this.connection.onInitialized(this.initialized.bind(this))
|
||||
this.connection.onExit(this.exit.bind(this))
|
||||
this.connection.onDidChangeConfiguration(this.configurationChanged.bind(this))
|
||||
|
||||
// documents handler
|
||||
this.documents = new TextDocuments()
|
||||
|
||||
// add handlers to documents
|
||||
this.documents.onDidOpen(this.documentOpened.bind(this))
|
||||
this.documents.onDidChangeContent(this.documentChanged.bind(this))
|
||||
|
||||
// Attach LSP features
|
||||
this.connection.onCompletion(this.getCompletion.bind(this))
|
||||
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
this.documents.listen(this.connection)
|
||||
|
||||
// Listen on the connection
|
||||
this.connection.listen()
|
||||
}
|
||||
|
||||
getCompletion (textDocumentPosition) {
|
||||
// TODO check if completion items can be proposed in right context -> new feature, currently items are proposed everywhere
|
||||
return this.itemsCompletionProvider.completionItems
|
||||
}
|
||||
|
||||
initialize (params) {
|
||||
// const capabilities = params.capabilities
|
||||
// TODO check if client supports capabilities? Might make no sense as we support vscode only currently, but will make sense when used in other lsp clients
|
||||
return {
|
||||
capabilities: {
|
||||
textDocumentSync: this.documents.syncKind,
|
||||
completionProvider: { resolveProvider: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async initialized () {
|
||||
this.globalSettings = await this.getGlobalConfig()
|
||||
|
||||
// Register for all configuration changes.
|
||||
this.connection.client.register(DidChangeConfigurationNotification.type, undefined)
|
||||
await this.initializeItemCompletionProvider()
|
||||
}
|
||||
|
||||
async initializeItemCompletionProvider () {
|
||||
this.itemsCompletionProvider = new ItemCompletionProvider()
|
||||
// TODO what todo here if it fails?
|
||||
const err = await this.itemsCompletionProvider.start(this.globalSettings.host, this.globalSettings.port)
|
||||
return err
|
||||
}
|
||||
|
||||
exit () {
|
||||
if (this.itemsCompletionProvider) {
|
||||
this.itemsCompletionProvider.stop()
|
||||
}
|
||||
}
|
||||
|
||||
configurationChanged (change) {
|
||||
// sometimes we get an empty settings object
|
||||
if (!change.settings || !change.settings.openhab) {
|
||||
return
|
||||
}
|
||||
this.globalSettings = change.settings.openhab
|
||||
if (this.itemsCompletionProvider) {
|
||||
this.itemsCompletionProvider.restartIfConfigChanged(this.globalSettings.host, this.globalSettings.port)
|
||||
}
|
||||
// Revalidate all open text documents - not needed right now but might make sense based on settings for validation
|
||||
// this.documents.all().forEach(this.validateDocument)
|
||||
}
|
||||
|
||||
documentOpened (event) {
|
||||
this.validateDocument(event.document)
|
||||
}
|
||||
|
||||
documentChanged (event) {
|
||||
this.validateDocument(event.document)
|
||||
}
|
||||
|
||||
validateDocument (document) {
|
||||
const diagnostics = validateTextDocument(document, this.globalSettings)
|
||||
this.connection.sendDiagnostics(diagnostics)
|
||||
}
|
||||
|
||||
async getGlobalConfig () {
|
||||
const result = this.connection.workspace.getConfiguration({
|
||||
section: 'openhab'
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Server
|
|
@ -1,19 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"types":[
|
||||
"node"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
]
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./client" }
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue