Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental FileSystemProvider for API objects #257

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
823 changes: 17 additions & 806 deletions client/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"axios": "^0.21.1",
"copy-paste": "^1.3.0",
"lodash": "^4.17.20",
"vscode-languageclient": "6.0.0-next.1"
"vscode-languageclient": "6.0.0-next.1",
"yaml": "^1.10.2"
},
"devDependencies": {
"@types/vscode": "^1.14.0"
Expand Down
112 changes: 112 additions & 0 deletions client/src/FilesystemProvider/FilesystemProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { EventEmitter } from 'vscode';
import { FileSystemError } from 'vscode';
import { Event, FileChangeEvent, Uri, Disposable, FileStat, FileType } from 'vscode';
import {
FileSystemProvider
} from 'vscode'
import * as YAML from 'yaml'
import { ItemsModel } from '../ItemsExplorer/ItemsModel';
import { ThingsModel } from '../ThingsExplorer/ThingsModel'

export class File implements FileStat {

type: FileType;
ctime: number;
mtime: number;
size: number;

name: string;
data?: Uint8Array;

constructor(name: string) {
this.type = FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
}
}

export class Directory implements FileStat {

type: FileType;
ctime: number;
mtime: number;
size: number;

name: string;
entries: Map<string, File | Directory>;

constructor(name: string) {
this.type = FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
this.entries = new Map();
}
}

export type Entry = File | Directory;

export class OHFileSystemProvider implements FileSystemProvider {
private _emitter = new EventEmitter<FileChangeEvent[]>()
root = new Directory('');

onDidChangeFile: Event<FileChangeEvent[]> = this._emitter.event

watch(uri: Uri, options: { recursive: boolean; excludes: string[]; }): Disposable {
return new Disposable(() => { })
}
stat(uri: Uri): FileStat | Thenable<FileStat> {
if (uri.toString().endsWith('.yaml')) return new File(uri.toString().substring(uri.toString().indexOf('/')))
return this.root
}
readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]> {
if (uri.path !== '/') throw FileSystemError.FileNotFound()
// temp
return Promise.resolve([
['1.yaml', FileType.File],
['2.yaml', FileType.File],
['3.yaml', FileType.File],
])
}
createDirectory(uri: Uri): void | Thenable<void> {
throw FileSystemError.NoPermissions()
return Promise.reject('Method not implemented.');
}
async readFile(uri: Uri): Promise<Uint8Array> {
if (!uri.toString().endsWith('.yaml') && !uri.toString().endsWith('.json')) throw FileSystemError.FileIsADirectory()

try {
if (uri.path.startsWith('/things/')) {
const uid = uri.path.replace(/^\/things\//, '').replace(/\.yaml$/, '')
const thingsModel = new ThingsModel()
const thing = await thingsModel.get(uid)
return Buffer.from(`# yaml-language-server: $schema=openhab:/schemas/thing-types/${thing.UID.split(':')[0] + ':' + thing.UID.split(':')[1]}.json\n` + YAML.stringify(thing))
} else if (uri.path.startsWith('/items/')) {
const name = uri.path.replace(/^\/items\//, '').replace(/\.yaml$/, '')
const itemsModel = new ItemsModel()
const item = await itemsModel.get(name)
return Buffer.from(YAML.stringify(item))
}
} catch (ex) {
throw FileSystemError.FileNotFound()
}

throw FileSystemError.FileNotFound()
}
writeFile(uri: Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable<void> {
throw new Error('Method not implemented.');
}
delete(uri: Uri, options: { recursive: boolean; }): void | Thenable<void> {
throw new Error('Method not implemented.');
}
rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean; }): void | Thenable<void> {
throw new Error('Method not implemented.');
}
copy?(source: Uri, destination: Uri, options: { overwrite: boolean; }): void | Thenable<void> {
throw new Error('Method not implemented.');
}

}
111 changes: 111 additions & 0 deletions client/src/FilesystemProvider/JSONSchemaProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as vscode from 'vscode'
import * as utils from '../Utils/Utils'
import axios, { AxiosRequestConfig } from 'axios'

const SCHEMA = "openhab"

const itemsSchema = JSON.stringify({
'$schema': 'https://json-schema.org/draft/2020-12/schema',
'$id': 'openhab:/schemas/items.json',
title: 'Item',
properties: {
name: {
description: 'The name of the item',
type: 'string'
},
'label': {
description: 'The label of the item',
type: 'string'
},
'type': {
description: 'The type of the item',
type: 'string',
enum: ['Switch', 'Contact', 'String', 'Number', 'Dimmer', 'DateTime', 'Color', 'Image', 'Player', 'Location', 'Rollershutter', 'Call', 'Group']
},
},
required: ['name', 'type']
})

export class OHJSONSchemaProvider {
public async initialize() {
try {
const yamlExtensionAPI = await vscode.extensions.getExtension("redhat.vscode-yaml").activate()
yamlExtensionAPI.registerContributor(SCHEMA, this.onRequestSchemaURI, this.onRequestSchemaContent)
} finally {

}
}

public onRequestSchemaURI(resource: string) {
const parsedUri = vscode.Uri.parse(resource)
if (parsedUri.path.startsWith('/items')) {
return `${SCHEMA}://schemas/items.json`
} else if (parsedUri.path.startsWith('/things')) {
const uid = parsedUri.path.replace(/^\/things\//, '').replace(/\.yaml$/, '')
return `${SCHEMA}://schemas/thing-types/${uid.split(':')[0] + ':' + uid.split(':')[1]}.json`
}
}

public onRequestSchemaContent(schemaUri: string): Promise<string> {
const parsedUri = vscode.Uri.parse(schemaUri)
if (parsedUri.scheme !== SCHEMA) {
return Promise.reject()
}
if (!parsedUri.path || !parsedUri.path.startsWith('/')) {
return Promise.reject()
}

if (parsedUri.authority === 'schemas' && parsedUri.path === '/items.json') {
return Promise.resolve(itemsSchema)
}

if (parsedUri.authority === 'schemas' && parsedUri.path.startsWith('/thing-types')) {
const thingTypeUID = parsedUri.path.replace(/^\/thing-types\//, '').replace(/\.json$/, '')
const schema = {
'$schema': 'https://json-schema.org/draft/2020-12/schema',
'$id': `openhab:/schemas/thing-types/${thingTypeUID}.json`,
properties: {
UID: {
description: 'The UID of the thing',
type: 'string'
},
thingTypeUID: {
description: 'The thing type UID of the thing',
type: 'string'
},
label: {
description: 'The label of the thing',
type: 'string'
},
bridgeUID: {
description: 'The UID of the parent bridge the thing is attached to',
type: 'string'
},
configuration: {
description: 'The thing configuration',
properties: {}
}
},
require: ['UID', 'thingTypeUID', 'label', 'configuration']
}

let config: AxiosRequestConfig = {
url: utils.getHost() + '/rest/thing-types/' + thingTypeUID,
headers: {}
}

return axios(config).then((result) => {
result.data.configParameters.forEach((p) => {
schema.properties.configuration.properties[p.name] = {
title: p.label,
description: p.description
}
})

return Promise.resolve(JSON.stringify(schema))
})
}

return Promise.reject()
}
}
8 changes: 8 additions & 0 deletions client/src/ItemsExplorer/ItemsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ export class ItemsModel {
})
}

/**
* Returns an item by its name
* @param name the item name
*/
public get(name: String): Thenable<Item> {
return this.sendRequest(utils.getHost() + '/rest/items/' + name, (item: Item[]) => [item]).then((items) => Promise.resolve(items[0]))
}

/**
* List of items used in ItemsCompletion
*/
Expand Down
9 changes: 9 additions & 0 deletions client/src/ThingsExplorer/ThingsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export class ThingsModel {
return thing.channels ? thing.channels : []
}

/**
* Returns a thing by its UID
* @param uid thing UID
* @returns a Thing object
*/
public get(uid: string): Thenable<Thing> {
return this.sendRequest(utils.getHost() + '/rest/things/' + uid, (thing) => [thing]).then((things) => Promise.resolve(things[0]))
}

private sendRequest(uri: string, transform): Thenable<Thing[]> {
let config: AxiosRequestConfig = {
url: uri || utils.getHost() + '/rest/things',
Expand Down
18 changes: 18 additions & 0 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { ConfigManager } from './Utils/ConfigManager'
import { UpdateNoticePanel } from './WebViews/UpdateNoticePanel'
import { OH_CONFIG_PARAMETERS } from './Utils/types'
import { MigrationManager } from './Utils/MigrationManager'
import { OHFileSystemProvider } from './FilesystemProvider/FilesystemProvider'
import { OHJSONSchemaProvider } from './FilesystemProvider/JSONSchemaProvider'

let _extensionPath: string
let ohStatusBarItem: vscode.StatusBarItem
Expand Down Expand Up @@ -133,6 +135,11 @@ async function init(disposables: vscode.Disposable[], context: vscode.ExtensionC
ruleProvider.addRule()
}))

disposables.push(vscode.commands.registerCommand('openhab.command.items.openAsYAML', (query: Item) => {
vscode.workspace.openTextDocument(vscode.Uri.parse('openhab:/items/' + query.name + '.yaml'))
.then((doc) => vscode.window.showTextDocument(doc))
}))

disposables.push(vscode.commands.registerCommand('openhab.command.items.addToSitemap', (query: Item) => {
const sitemapProvider = new SitemapPartialProvider(query)
sitemapProvider.addToSitemap()
Expand All @@ -146,6 +153,17 @@ async function init(disposables: vscode.Disposable[], context: vscode.ExtensionC
disposables.push(vscode.commands.registerCommand('openhab.command.things.copyUID', (query) =>
ncp.copy(query.UID || query.uid)))

disposables.push(vscode.commands.registerCommand('openhab.command.things.openAsYAML', (query: Thing) => {
vscode.workspace.openTextDocument(vscode.Uri.parse('openhab:/things/' + query.UID + '.yaml'))
.then((doc) => vscode.window.showTextDocument(doc))
}))

const fsProvider = new OHFileSystemProvider()
const jsonSchemaProvider = new OHJSONSchemaProvider()
jsonSchemaProvider.initialize()

disposables.push(vscode.workspace.registerFileSystemProvider('openhab', fsProvider))


disposables.push(vscode.languages.registerHoverProvider({ language: 'openhab', scheme: 'file'}, {

Expand Down
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@
"command": "openhab.updateNotice",
"category": "openHAB",
"title": "Open the latest update notice"
},
{
"command": "openhab.debug.addFSToWorkspace",
"title": "openHAB: Add filesystem to workspace (debug)"
},
{
"command": "openhab.command.items.openAsYAML",
"title": "Open as YAML"
},
{
"command": "openhab.command.things.openAsYAML",
"title": "Open as YAML"
}
],
"configuration": {
Expand Down Expand Up @@ -307,6 +319,10 @@
"command": "openhab.command.items.addToSitemap",
"when": "view == openhabItems"
},
{
"command": "openhab.command.items.openAsYAML",
"when": "view == openhabItems"
},
{
"command": "openhab.command.items.copyState",
"when": "view == openhabItems && viewItem != statelessItem && viewItem != statelessGroup"
Expand All @@ -332,6 +348,10 @@
"command": "openhab.command.things.docs",
"when": "view == openhabThings && viewItem == thing",
"group": "inline"
},
{
"command": "openhab.command.things.openAsYAML",
"when": "view == openhabThings && viewItem == thing"
}
],
"view/title": [
Expand Down
Loading