From 8b62397ca50f0a9e8bfedf62b6d0dd7f0fee5953 Mon Sep 17 00:00:00 2001 From: Kuba Wolanin Date: Fri, 7 Jul 2017 12:58:40 +0200 Subject: [PATCH] Items Explorer view - Initial contribution (#20) Items Explorer view - Initial contribution * Items Explorer - added Item icons + cleanup * Rule template PR discussion follow-up. Added "Copy State" context menu for Items. Removed ongoing "Find References" feature. General code cleanup. Added Item code completions - Closes #7 * Documented code + updated changelog * Version bump Signed-off-by: Kuba Wolanin (github: kubawolanin) --- .gitignore | 3 +- AUTHORS | 2 + CHANGELOG.md | 12 ++- CONTRIBUTING.md | 17 ++++ package.json | 146 ++++++++++++++++++++------- resources/dark/color.svg | 5 + resources/dark/contact.svg | 4 + resources/dark/datetime.svg | 5 + resources/dark/dimmer.svg | 4 + resources/dark/group.svg | 4 + resources/dark/number.svg | 4 + resources/dark/player.svg | 4 + resources/dark/refresh.svg | 1 + resources/dark/rollershutter.svg | 9 ++ resources/dark/string.svg | 4 + resources/dark/switch.svg | 4 + resources/light/color.svg | 5 + resources/light/contact.svg | 4 + resources/light/datetime.svg | 5 + resources/light/dimmer.svg | 4 + resources/light/group.svg | 4 + resources/light/number.svg | 4 + resources/light/player.svg | 4 + resources/light/refresh.svg | 1 + resources/light/rollershutter.svg | 9 ++ resources/light/string.svg | 4 + resources/light/switch.svg | 4 + src/ContentProvider/openHAB.ts | 10 +- src/ItemsExplorer/IItem.ts | 58 +++++++++++ src/ItemsExplorer/Item.ts | 108 ++++++++++++++++++++ src/ItemsExplorer/ItemsCompletion.ts | 65 ++++++++++++ src/ItemsExplorer/ItemsExplorer.ts | 85 ++++++++++++++++ src/ItemsExplorer/ItemsModel.ts | 79 +++++++++++++++ src/ItemsExplorer/RuleProvider.ts | 53 ++++++++++ src/Utils.ts | 78 ++++++++++++++ src/extension.ts | 126 +++++++++++------------ 36 files changed, 831 insertions(+), 107 deletions(-) create mode 100644 resources/dark/color.svg create mode 100644 resources/dark/contact.svg create mode 100644 resources/dark/datetime.svg create mode 100644 resources/dark/dimmer.svg create mode 100644 resources/dark/group.svg create mode 100644 resources/dark/number.svg create mode 100644 resources/dark/player.svg create mode 100644 resources/dark/refresh.svg create mode 100644 resources/dark/rollershutter.svg create mode 100644 resources/dark/string.svg create mode 100644 resources/dark/switch.svg create mode 100644 resources/light/color.svg create mode 100644 resources/light/contact.svg create mode 100644 resources/light/datetime.svg create mode 100644 resources/light/dimmer.svg create mode 100644 resources/light/group.svg create mode 100644 resources/light/number.svg create mode 100644 resources/light/player.svg create mode 100644 resources/light/refresh.svg create mode 100644 resources/light/rollershutter.svg create mode 100644 resources/light/string.svg create mode 100644 resources/light/switch.svg create mode 100644 src/ItemsExplorer/IItem.ts create mode 100644 src/ItemsExplorer/Item.ts create mode 100644 src/ItemsExplorer/ItemsCompletion.ts create mode 100644 src/ItemsExplorer/ItemsExplorer.ts create mode 100644 src/ItemsExplorer/ItemsModel.ts create mode 100644 src/ItemsExplorer/RuleProvider.ts create mode 100644 src/Utils.ts diff --git a/.gitignore b/.gitignore index dcd8530..47c931a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ out node_modules -*.vsix \ No newline at end of file +*.vsix +*.todo \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index c201b22..22d5733 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,8 @@ # This file lists all individuals having contributed content to the repository. # For how it is generated, see `project-orga/generate-authors.sh`. +Claudio Spizzi +Dennis Gieseler Kai Kreuzer Kuba Wolanin Thomas Dietrich diff --git a/CHANGELOG.md b/CHANGELOG.md index 81de567..3b68359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # openHAB VS Code Extension Change Log -## Unreleased -- Add icon theme +## 0.1.0 - 2017-07-07 +- Completely new openHAB Items Explorer view in the sidebar! + - Preview **all** of your items thanks to the REST API + - Dynamic rules from the Items Explorer view - including the current state + - Ability to copy Item's name and state + - Clicking non-Group item opens it in the Paper UI by default + - Note: Currently in VS Code stable Items Explorer is permanently visible. VS Code Insiders allows you to hide the tree view thanks to [vscode#29436](https://github.com/Microsoft/vscode/issues/29436) +- Added Items autocompletion (with IntelliSense documentation) (#7) +- Quick search in the Community Forum +- Added icon theme ## 0.0.2 - 2017-06-21 - openHAB hostname and port are now configurable through user or workspace settings (#14) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af530b4..1e7f3c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -255,3 +255,20 @@ general guidelines for the community as a whole: respond to an email you are potentially sending to a large number of people. Please consider this before you update. Also remember that nobody likes spam. + +## Attributions + +The following icons were used from [Material Design Iconset](material.io/icons/) (available under the Apache License Version 2.0): + +| Item type | Icon name | +|--|--| +| Color | `ic_format_color_fill_black_24px` | +| Contact | `ic_flip_black_24px` | +| DateTime | `ic_access_time_black_24px` | +| Dimmer | `ic_brightness_medium_black_24px` | +| Switch | `ic_radio_button_checked_black_24px` | +| String | `ic_view_headline_black_24px` | +| Group | `ic_folder_open_black_24px` | +| Number | `ic_dialpad_black_24px` | +| Player | `ic_play_circle_outline_black_24px` | +| Rollershutter | `ic_line_weight_black_24px` | \ No newline at end of file diff --git a/package.json b/package.json index 6e76e39..1d64c69 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,14 @@ "name": "openhab", "displayName": "openHAB", "description": "openHAB syntax highlight, code snippets, integrated Basic UI preview and docs search", - "version": "0.0.2", + "version": "0.1.0", "publisher": "openhab", "icon": "openhab.png", "repository": { "type": "git", "url": "https://github.com/openhab/openhab-vscode.git" }, + "license": "SEE LICENSE IN LICENSE", "engines": { "vscode": "^1.13.0" }, @@ -21,41 +22,97 @@ "onCommand:openhab.searchDocs", "onCommand:openhab.searchCommunity", "onCommand:openhab.basicUI", + "onCommand:openhab.command.items.refreshEntry", + "onCommand:openhab.command.items.showInPaperUI", + "onCommand:openhab.command.items.copyName", + "onCommand:openhab.command.items.copyState", + "onCommand:openhab.command.items.addRule", "onLanguage:openhab" ], "main": "./out/src/extension", "contributes": { "menus": { "editor/title": [{ - "when": "resourceLangId == openhab", - "command": "openhab.basicUI", - "group": "navigation" - }] + "when": "resourceLangId == openhab", + "command": "openhab.basicUI", + "group": "navigation" + }], + "view/title": [{ + "command": "openhab.command.items.refreshEntry", + "when": "view == openhabItems", + "group": "navigation" + }], + "view/item/context": [{ + "command": "openhab.command.items.showInPaperUI", + "when": "view == openhabItems" + }, + { + "command": "openhab.command.items.copyName", + "when": "view == openhabItems" + }, + { + "command": "openhab.command.items.copyState", + "when": "view == openhabItems && viewItem != statelessItem && viewItem != statelessGroup" + }, + { + "command": "openhab.command.items.addRule", + "when": "view == openhabItems" + } + ] }, "keybindings": [{ "when": "resourceLangId == openhab", "command": "openhab.basicUI", "key": "ctrl+alt+o", "mac": "cmd+alt+o" - }, { + }, + { "when": "resourceLangId == openhab", "command": "openhab.searchDocs", "key": "shift+alt+o" - }], + } + ], "commands": [{ "command": "openhab.searchDocs", "title": "openHAB: Search in Docs" - }, { + }, + { "command": "openhab.searchCommunity", "title": "openHAB: Search in Community Forum" - }, { + }, + { + "command": "openhab.command.items.showInPaperUI", + "title": "Show in Paper UI" + }, + { + "command": "openhab.command.items.addRule", + "title": "Create a Rule" + }, + { + "command": "openhab.command.items.copyName", + "title": "Copy Name" + }, + { + "command": "openhab.command.items.copyState", + "title": "Copy State" + }, + { + "command": "openhab.command.items.refreshEntry", + "title": "Refresh", + "icon": { + "light": "resources/light/refresh.svg", + "dark": "resources/dark/refresh.svg" + } + }, + { "command": "openhab.basicUI", "title": "openHAB: Open Basic UI", "icon": { "light": "./images/oh_color.svg", "dark": "./images/oh.svg" } - }], + } + ], "configuration": { "type": "object", "title": "openHAB Configuration", @@ -78,35 +135,46 @@ } }, "languages": [{ - "id": "openhab", - "aliases": ["openHAB"], - "extensions": [ - ".rules", - ".script", - ".items", - ".sitemap", - ".things", - ".persist" - ], - "configuration": "./language-configuration.json" - }], + "id": "openhab", + "aliases": [ + "openHAB" + ], + "extensions": [ + ".rules", + ".script", + ".items", + ".sitemap", + ".things", + ".persist" + ], + "configuration": "./language-configuration.json" + }], "grammars": [{ - "language": "openhab", - "scopeName": "source.openhab", - "path": "./syntaxes/openhab.tmLanguage.json" - }], + "language": "openhab", + "scopeName": "source.openhab", + "path": "./syntaxes/openhab.tmLanguage.json" + }], + "views": { + "explorer": [{ + "id": "openhabItems", + "name": "openHAB Items", + "when": "resourceLangId == openhab" + }] + }, "snippets": [{ "language": "openhab", "path": "./snippets/openhabrules.json" - }, { + }, + { "language": "openhab", "path": "./snippets/openhab.json" - }], + } + ], "iconThemes": [{ - "id": "openhab", - "label": "openHAB", - "path": "./fileicons/openhab-icon-theme.json" - }] + "id": "openhab", + "label": "openHAB", + "path": "./fileicons/openhab-icon-theme.json" + }] }, "scripts": { "vscode:prepublish": "tsc -p ./", @@ -115,14 +183,18 @@ "test": "node ./node_modules/vscode/bin/test" }, "devDependencies": { + "@types/node": "^6.0.40", + "@types/mocha": "^2.2.32", "typescript": "^2.0.3", "vscode": "^1.0.0", - "mocha": "^2.3.3", - "@types/node": "^6.0.40", - "@types/mocha": "^2.2.32" + "mocha": "^2.3.3" }, "dependencies": { "@types/lodash": "^4.14.58", - "lodash": "^4.17.4" + "@types/request-promise-native": "^1.0.5", + "copy-paste": "^1.3.0", + "lodash": "^4.17.4", + "request": "^2.81.0", + "request-promise-native": "^1.0.4" } -} +} \ No newline at end of file diff --git a/resources/dark/color.svg b/resources/dark/color.svg new file mode 100644 index 0000000..d692732 --- /dev/null +++ b/resources/dark/color.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/dark/contact.svg b/resources/dark/contact.svg new file mode 100644 index 0000000..d3044ca --- /dev/null +++ b/resources/dark/contact.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/datetime.svg b/resources/dark/datetime.svg new file mode 100644 index 0000000..432b776 --- /dev/null +++ b/resources/dark/datetime.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/dark/dimmer.svg b/resources/dark/dimmer.svg new file mode 100644 index 0000000..0fd547e --- /dev/null +++ b/resources/dark/dimmer.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/group.svg b/resources/dark/group.svg new file mode 100644 index 0000000..dfa5cbb --- /dev/null +++ b/resources/dark/group.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/number.svg b/resources/dark/number.svg new file mode 100644 index 0000000..93e1f69 --- /dev/null +++ b/resources/dark/number.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/player.svg b/resources/dark/player.svg new file mode 100644 index 0000000..511aa6b --- /dev/null +++ b/resources/dark/player.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/refresh.svg b/resources/dark/refresh.svg new file mode 100644 index 0000000..d79fdaa --- /dev/null +++ b/resources/dark/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/dark/rollershutter.svg b/resources/dark/rollershutter.svg new file mode 100644 index 0000000..8dac042 --- /dev/null +++ b/resources/dark/rollershutter.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/dark/string.svg b/resources/dark/string.svg new file mode 100644 index 0000000..08f5679 --- /dev/null +++ b/resources/dark/string.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/dark/switch.svg b/resources/dark/switch.svg new file mode 100644 index 0000000..a76f1a1 --- /dev/null +++ b/resources/dark/switch.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/color.svg b/resources/light/color.svg new file mode 100644 index 0000000..c69312c --- /dev/null +++ b/resources/light/color.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/light/contact.svg b/resources/light/contact.svg new file mode 100644 index 0000000..38336ec --- /dev/null +++ b/resources/light/contact.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/datetime.svg b/resources/light/datetime.svg new file mode 100644 index 0000000..a3e3fa1 --- /dev/null +++ b/resources/light/datetime.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/light/dimmer.svg b/resources/light/dimmer.svg new file mode 100644 index 0000000..9fc3005 --- /dev/null +++ b/resources/light/dimmer.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/group.svg b/resources/light/group.svg new file mode 100644 index 0000000..2e859c8 --- /dev/null +++ b/resources/light/group.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/number.svg b/resources/light/number.svg new file mode 100644 index 0000000..d62a954 --- /dev/null +++ b/resources/light/number.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/player.svg b/resources/light/player.svg new file mode 100644 index 0000000..0a60f6b --- /dev/null +++ b/resources/light/player.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/refresh.svg b/resources/light/refresh.svg new file mode 100644 index 0000000..e034574 --- /dev/null +++ b/resources/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/light/rollershutter.svg b/resources/light/rollershutter.svg new file mode 100644 index 0000000..a332429 --- /dev/null +++ b/resources/light/rollershutter.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/light/string.svg b/resources/light/string.svg new file mode 100644 index 0000000..787268c --- /dev/null +++ b/resources/light/string.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/switch.svg b/resources/light/switch.svg new file mode 100644 index 0000000..ad53823 --- /dev/null +++ b/resources/light/switch.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/ContentProvider/openHAB.ts b/src/ContentProvider/openHAB.ts index 756c7ac..fbf69ef 100644 --- a/src/ContentProvider/openHAB.ts +++ b/src/ContentProvider/openHAB.ts @@ -1,9 +1,9 @@ import { - Event, - Uri, CancellationToken, + Event, + EventEmitter, TextDocumentContentProvider, - EventEmitter + Uri } from 'vscode' import defaults = require('lodash/defaults') @@ -48,7 +48,7 @@ iframe { - ` @@ -67,4 +67,4 @@ export class OpenHABContentProvider implements TextDocumentContentProvider { provideTextDocumentContent(uri: Uri, token: CancellationToken): string | Thenable { return HTML_CONTENT(decodeOpenHABUri(uri)) } -} \ No newline at end of file +} diff --git a/src/ItemsExplorer/IItem.ts b/src/ItemsExplorer/IItem.ts new file mode 100644 index 0000000..9d50332 --- /dev/null +++ b/src/ItemsExplorer/IItem.ts @@ -0,0 +1,58 @@ +import { Uri } from 'vscode' + +/** + * Interface describing an openHAB Item structure + * + * Kuba Wolanin - Initial contribution + */ +export interface IItem { + /** + * Direct URL to openHAB item + */ + link?: string | Uri; + + /** + * Current state of the item, e.g. "OFF" or "22" + */ + state: string; + + /** + * Format of the state visible to the end user + */ + stateDescription?: { pattern: string; readOnly: boolean; options: any[] }; + + /** + * openHAB Item type, e.g. "String", "Number", "DateTime" + */ + type: string; + + /** + * Unique item's name, e.g. "Kitchen_Door" + */ + name: string; + + /** + * Human readable label, e.g. "Kitchen door sensor" + */ + label: string; + + /** + * Used for the ESH icon set, e.g. "kitchen" + */ + category?: string; + + /** + * Indicates if the item is a Group type + */ + members?: IItem[]; + + /** + * Items tags. + */ + tags?: string[]; + + /** + * Array of Groups the Item belongs to + */ + groupNames?: string[]; +} \ No newline at end of file diff --git a/src/ItemsExplorer/Item.ts b/src/ItemsExplorer/Item.ts new file mode 100644 index 0000000..74922af --- /dev/null +++ b/src/ItemsExplorer/Item.ts @@ -0,0 +1,108 @@ +import { IItem } from './IItem' +import * as _ from 'lodash' + +export class Item { + + constructor(private item: IItem) { + } + + /** + * 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' + */ + public get name(): string { + 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 + */ + public get type(): string { + return this.item.type; + } + + /** + * 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" + */ + public get label(): string { + return this.item.label; + } + + /** + * 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' + */ + public get state(): string { + var nullType: string[] = ['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' + */ + public get link(): string { + return this.item.link.toString(); + } + + /** + * Relative path to item's icon + * e.g. '/icon/kitchen?format=svg' + */ + public get icon(): string { + 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. + */ + public get isGroup(): boolean { + return this.item.type === 'Group' + } + + /** + * True if the item doesn't belong to any group + */ + public get isRootItem(): boolean { + 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. + */ + public get tags(): string[] { + return this.item.tags + } + + /** + * Returns an Array of Groups the item belongs to. + */ + public get groupNames(): string[] { + return this.item.groupNames + } + + /** + * True if type of the item is equal to 'Group' + */ + public get members(): IItem[] { + return this.isGroup && this.item.members + } +} diff --git a/src/ItemsExplorer/ItemsCompletion.ts b/src/ItemsExplorer/ItemsCompletion.ts new file mode 100644 index 0000000..942abd9 --- /dev/null +++ b/src/ItemsExplorer/ItemsCompletion.ts @@ -0,0 +1,65 @@ +import { + CancellationToken, + CompletionItem, + CompletionItemKind, + CompletionItemProvider, + CompletionList, + Position, + TextDocument +} from 'vscode' + +import { Item } from './Item' +import { ItemsModel } from './ItemsModel' +import * as _ from 'lodash' + +/** + * Produces a list of openHAB items completions + * collected from REST API + * + * Kuba Wolanin - Initial contribution + */ +export class ItemsCompletion implements CompletionItemProvider { + + constructor(private openhabHost: string) { + if (!this.model) { + this.model = new ItemsModel(this.openhabHost) + } + } + + private model: ItemsModel + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Thenable { + return new Promise((resolve, reject) => { + this.model.completions.then(completions => { + resolve(completions.map((item: Item) => { + let completionItem = _.assign(new CompletionItem(item.name), { + kind: CompletionItemKind.Variable, + detail: item.type, + documentation: this.getDocumentation(item), + }) + + return completionItem + })) + }) + }) + } + + /** + * Generates a documentation string for the IntelliSense auto-completion + * Contains Item's label, state, tags and group names. + * @param item openHAB Item + */ + private getDocumentation(item: Item): string { + let label = item.label ? item.label + ' ' : '' + let state = item.state ? '(' + item.state + ')' : '' + let tags = item.tags.length && 'Tags: ' + item.tags.join(', ') + let groupNames = item.groupNames.length && 'Groups: ' + item.groupNames.join(', ') + let documentation: string[] = [ + label + state, + tags, + groupNames + ] + + return _.compact(documentation).join('\n') + } +} diff --git a/src/ItemsExplorer/ItemsExplorer.ts b/src/ItemsExplorer/ItemsExplorer.ts new file mode 100644 index 0000000..6b07429 --- /dev/null +++ b/src/ItemsExplorer/ItemsExplorer.ts @@ -0,0 +1,85 @@ +import { + Event, + EventEmitter, + TreeDataProvider, + TreeItem, + TreeItemCollapsibleState +} from 'vscode' + +import { Item } from './Item' +import { ItemsModel } from './ItemsModel' +import * as path from 'path' + +/** + * Produces a tree view of openHAB items + * collected from REST API + * + * Kuba Wolanin - Initial contribution + */ +export class ItemsExplorer implements TreeDataProvider { + + private _onDidChangeTreeData: EventEmitter = new EventEmitter() + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event + + constructor(private openhabHost: string) { + } + + private model: ItemsModel + + refresh(): void { + this._onDidChangeTreeData.fire() + } + + public getTreeItem(item: Item): TreeItem { + return { + label: item.name + (item.state ? ' (' + item.state + ')' : ''), + collapsibleState: item.isGroup ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None, + command: item.isGroup ? void 0 : { + command: 'openhab.command.items.showInPaperUI', + arguments: [item.name], + title: 'Show in Paper UI' + }, + contextValue: this.getViewItem(item), + iconPath: { + light: this.getIcon('light', item.type), + dark: this.getIcon('dark', item.type) + } + }; + } + + /** + * Used to determine a context value of the TreeItem + * If viewItem is 'statelessItem' or 'statelessGroup', + * "Copy State" context menu doesn't show up. + * + * @param item Item + */ + private getViewItem(item): string { + let type = item.isGroup ? 'Group' : 'Item' + return item.state ? type : 'stateless' + type + } + + /** + * Returns an absolute path to the Item's type icon + * Note: VS Code doesn't allow to display icons from external source. + * This is why `item.icon` property is not used there. + * + * @param shade 'light' or 'dark' depending on the Color Theme + * @param name icon's filename + */ + private getIcon(shade: string, name: string) { + return path.join(__filename, '..', '..', '..', '..', 'resources', shade, name.toLowerCase() + '.svg') + } + + public getChildren(item?: Item): Item[] | Thenable { + if (!item) { + if (!this.model) { + this.model = new ItemsModel(this.openhabHost) + } + + return this.model.roots + } + + return this.model.getChildren(item) + } +} diff --git a/src/ItemsExplorer/ItemsModel.ts b/src/ItemsExplorer/ItemsModel.ts new file mode 100644 index 0000000..e392792 --- /dev/null +++ b/src/ItemsExplorer/ItemsModel.ts @@ -0,0 +1,79 @@ +import { window } from 'vscode' +import { Item } from './Item' + +import * as _ from 'lodash' +import * as request from 'request-promise-native' + +/** + * Collects Items in JSON format from REST API + * and transforms it into sorted tree + * + * Kuba Wolanin - Initial contribution + */ +export class ItemsModel { + + constructor(private host: string) { + } + + /** + * Returns Items that don't belong to any Group + */ + public get roots(): Thenable { + return this.sendRequest(null, (items: Item[]) => { + let itemsMap = items.map(item => new Item(item)) + let rootItems = _.filter(itemsMap, (item: Item) => item.isRootItem) + return rootItems + }) + } + + /** + * Returns members of Group-type Item + * @param item openHAB root Item + */ + public getChildren(item: Item): Thenable { + return this.sendRequest(item.link, (item: Item) => { + let itemsMap = item.members.map(item => new Item(item)) + return itemsMap + }) + } + + /** + * List of items used in ItemsCompletion + */ + public get completions(): Thenable { + return this.sendRequest(null, (items: Item[]) => { + return items + }) + } + + private sendRequest(uri: string, transform) { + let options = { + uri: uri || this.host + '/rest/items', + json: true + } + + return new Promise(resolve => { + request(options) + .then(function (response: Item[] | Item) { + resolve(this.sort(transform(response))) + }.bind(this)) + .catch(err => { + window.showErrorMessage('Error while connecting: ' + err.message); + }) + }) + } + + protected sort(nodes: Item[]): Item[] { + return nodes.sort((n1, n2) => { + if (n1.isGroup && !n2.isGroup) { + return -1 + } + + if (!n1.isGroup && n2.isGroup) { + return 1 + } + + return n1.name.localeCompare(n2.name) + }); + } +} diff --git a/src/ItemsExplorer/RuleProvider.ts b/src/ItemsExplorer/RuleProvider.ts new file mode 100644 index 0000000..9d2c964 --- /dev/null +++ b/src/ItemsExplorer/RuleProvider.ts @@ -0,0 +1,53 @@ +import { + Selection, + SnippetString, + TextDocument, + window +} from 'vscode' + +import { Item } from './Item' + +const RULE_TEMPLATE = (item: Item): SnippetString => { + let label = item.label && item.label !== item.name ? `${item.label} (${item.name})` : `${item.name}` + let state = item.type === 'String' ? `"${item.state}"` : `${item.state}` + let statePart = item.state ? ' changed from ' + state : ' // your condition here' + + return new SnippetString(` +rule "React on ` + label + ` change/update" +when + Item ${item.name}` + statePart + ` +then + // your logic here +end +`) +} + +/** + * Creates a dynamic rule within '*.rules' file + * + * Kuba Wolanin - Initial contribution + */ +export class RuleProvider { + + constructor(private item: Item) { + } + + /** + * Creates a dynamic rule snippet based on Item's properties. + * Note: this will insert the snippet only if + * currently open file has a `rules` extension. + */ + public addRule() { + let editor = window.activeTextEditor + let document = editor.document + + if (document.fileName.split('.')[1] === 'rules') { + let position = editor.selection.active + let newPosition = position.with(position.line, 0) + + editor.insertSnippet(RULE_TEMPLATE(this.item), newPosition) + } else { + window.showInformationMessage('Please open "*.rules" file in the editor to add a new rule.') + } + } +} diff --git a/src/Utils.ts b/src/Utils.ts new file mode 100644 index 0000000..8a62524 --- /dev/null +++ b/src/Utils.ts @@ -0,0 +1,78 @@ +import { + commands, + Uri, + ViewColumn, + window, + workspace +} from 'vscode' + +import { + Query, + SCHEME, + OpenHABContentProvider, + encodeOpenHABUri +} from './ContentProvider/openHAB' + +import * as _ from 'lodash' +import * as fs from 'fs' +import * as path from 'path' + +export function getHost() { + let config = workspace.getConfiguration('openhab') + let host = config.host + let protocol = 'http' + let port = config.port + + if (host.includes('://')) { + let split = host.split('://') + host = split[1] + protocol = split[0] + } + + return protocol + '://' + host + ':' + port +} + +export function pathExists(p: string): boolean { + try { + fs.accessSync(p); + } catch (err) { + return false; + } + + return true; +} + +export function isOpenHABWorkspace(): boolean { + let folders = ['items', 'rules', 'service', 'sitemap'] + return _.some(folders, (folder) => pathExists(path.join(workspace.rootPath, folder))) +} + +export function openHtml(uri: Uri, title) { + return commands.executeCommand('vscode.previewHtml', uri, ViewColumn.Two, title) + .then((success) => { + }, (reason) => { + window.showErrorMessage(reason) + }) +} + +export function openBrowser(url = 'http://docs.openhab.org/search?q=%s') { + let editor = window.activeTextEditor + if (!editor) { + window.showInformationMessage('No editor is active') + return + } + + let selection = editor.selection + let text = editor.document.getText(selection) + url = url.replace('%s', text.replace(' ', '%20')) + return commands.executeCommand('vscode.open', Uri.parse(url)) +} + +export function openUI(query?: Query, title = 'Basic UI', editor = window.activeTextEditor) { + let params: Query = { + hostname: getHost() + }; + + _.extend(params, query) + openHtml(encodeOpenHABUri(params), title) +} diff --git a/src/extension.ts b/src/extension.ts index b6397b8..4874fda 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,65 +1,48 @@ 'use strict'; import { - ExtensionContext, + commands, + CompletionItem, Disposable, - workspace, - window, + ExtensionContext, + languages, + TextDocumentChangeEvent, Uri, - commands, ViewColumn, - TextDocumentChangeEvent + window, + workspace } from 'vscode' import { - Query, SCHEME, - OpenHABContentProvider, - encodeOpenHABUri + OpenHABContentProvider } from './ContentProvider/openHAB' -import _ = require('lodash') -import path = require('path') - -function getHost() { - let config = workspace.getConfiguration('openhab') - if (!config.host) { - window.showInformationMessage('Please provide openHAB server hostname') - return - } - - return config.port ? config.host + ':' + config.port : config.host -} +import { + getHost, + isOpenHABWorkspace, + openBrowser, + openHtml, + openUI, + pathExists +} from './Utils' + +import { ItemsExplorer } from './ItemsExplorer/ItemsExplorer' +import { ItemsCompletion } from './ItemsExplorer/ItemsCompletion' +import { RuleProvider } from './ItemsExplorer/RuleProvider' +import { Item } from './ItemsExplorer/Item' + +import * as _ from 'lodash' +import * as ncp from 'copy-paste' +import * as path from 'path' async function init(context: ExtensionContext, disposables: Disposable[]): Promise { let ui = new OpenHABContentProvider() let registration = workspace.registerTextDocumentContentProvider(SCHEME, ui) + const itemsExplorer = new ItemsExplorer(getHost()) + const itemsCompletion = new ItemsCompletion(getHost()) - const openHtml = (uri: Uri, title) => { - return commands.executeCommand('vscode.previewHtml', uri, ViewColumn.Two, title) - .then((success) => { - }, (reason) => { - window.showErrorMessage(reason) - }) - } - - const openBrowser = (url = 'http://docs.openhab.org/search?q=%s') => { - let editor = window.activeTextEditor - if (!editor) { - window.showInformationMessage('No editor is active') - return - } - - let selection = editor.selection - let text = editor.document.getText(selection) - url = url.replace('%s', text.replace(' ', '%20')) - return commands.executeCommand('vscode.open', Uri.parse(url)) - } - - const openUI = (query?: Query, title = 'Basic UI', editor = window.activeTextEditor) => - openHtml(encodeOpenHABUri(query), title) - - let basicUI = commands.registerCommand('openhab.basicUI', () => { + disposables.push(commands.registerCommand('openhab.basicUI', () => { let editor = window.activeTextEditor if (!editor) { window.showInformationMessage('No editor is active') @@ -68,36 +51,55 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi let absolutePath = editor.document.fileName let fileName = path.basename(absolutePath) - let address = getHost() - - let params = { - hostname: address - }; if (fileName.split('.')[1] === 'sitemap') { let sitemap = fileName.split('.')[0] - - _.extend(params, { + return openUI({ route: '/basicui/app?sitemap=' + sitemap, - }) - - return openUI(params, sitemap) + }, sitemap + ' - Basic UI') } - return openUI(params) - }); + return openUI() + })) + + disposables.push(commands.registerCommand('openhab.searchDocs', () => openBrowser())) + + disposables.push(commands.registerCommand('openhab.searchCommunity', (phrase?) => { + let query: string = phrase || '%s' + openBrowser('https://community.openhab.org/search?q=' + query) + })) + + disposables.push(commands.registerCommand('openhab.command.items.showInPaperUI', (query?) => { + let param: string = query.name ? query.name : query + return openUI({ + route: '/paperui/index.html%23/configuration/item/edit/' + param + }, param + ' - Paper UI') + })) - let docs = commands.registerCommand('openhab.searchDocs', () => openBrowser()); + disposables.push(commands.registerCommand('openhab.command.items.refreshEntry', () => { + itemsExplorer.refresh() + })) - let community = commands.registerCommand('openhab.searchCommunity', () => - openBrowser('https://community.openhab.org/search?q=%s')); + disposables.push(commands.registerCommand('openhab.command.items.copyName', (query: Item) => + ncp.copy(query.name))) - disposables.push(basicUI, docs, community); + disposables.push(commands.registerCommand('openhab.command.items.copyState', (query: Item) => + ncp.copy(query.state))) + + disposables.push(commands.registerCommand('openhab.command.items.addRule', (query: Item) => { + let ruleProvider = new RuleProvider(query) + ruleProvider.addRule() + })) + + if (isOpenHABWorkspace()) { + disposables.push(window.registerTreeDataProvider('openhabItems', itemsExplorer)) + disposables.push(languages.registerCompletionItemProvider('openhab', itemsCompletion)) + } } export function activate(context: ExtensionContext) { const disposables: Disposable[] = []; - context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); + context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())) init(context, disposables) .catch(err => console.error(err));