diff --git a/README.md b/README.md index 7f9fdb8..3138ce7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # ABAP remote filesystem for visual studio code Ideally one day this will allow you to edit your ABAP code directly in Visual studio code -Very early stages, for now it only displays a list of packages +Very early stages, for now it only displays some packages and a handful of object types, no local objects,subroutines in object lists... + +Even things that do work need a big refactor +![image](https://user-images.githubusercontent.com/2453277/47466602-dd99dc00-d7e9-11e8-97ed-28e23dfd8f90.png) +syntax highlighting added manually with the [ABAP language extension](https://marketplace.visualstudio.com/items?itemName=larshp.vscode-abap),picture was too lame without it :) ## Features diff --git a/src/abap/AbapFunctionGroup.ts b/src/abap/AbapFunctionGroup.ts new file mode 100644 index 0000000..f38e684 --- /dev/null +++ b/src/abap/AbapFunctionGroup.ts @@ -0,0 +1,21 @@ +import { AbapObject } from "./AbapObject" +import { Uri } from "vscode" + +export class AbapFunctionGroup extends AbapObject { + isLeaf() { + return false + } + getUri(base: Uri): Uri { + const ptype = encodeURIComponent(this.type) + const pname = encodeURIComponent(this.name) + const techname = encodeURIComponent( + this.namespace() === "" + ? "SAPL" + this.name + : `/${this.namespace()}/SAPL${this.nameinns}` + ) + return base.with({ + path: "/sap/bc/adt/repository/nodestructure", + query: `parent_name=${pname}&parent_tech_name=${techname}&parent_type=${ptype}&withShortDescriptions=true` + }) + } +} diff --git a/src/abap/AbapFunctionModule.ts b/src/abap/AbapFunctionModule.ts new file mode 100644 index 0000000..4d2667d --- /dev/null +++ b/src/abap/AbapFunctionModule.ts @@ -0,0 +1,8 @@ +import { AbapObject } from "./AbapObject" +import { Uri } from "vscode" + +export class AbapFunctionModule extends AbapObject { + getUri(base: Uri): Uri { + return base.with({ path: this.path + "/source/main" }) + } +} diff --git a/src/abap/AbapObject.ts b/src/abap/AbapObject.ts new file mode 100644 index 0000000..ba1fc6b --- /dev/null +++ b/src/abap/AbapObject.ts @@ -0,0 +1,35 @@ +import { Uri } from "vscode" + +export interface AbapObjectPart { + type: string + name: string + parent: AbapObject +} + +export class AbapObject { + type: string + name: string + path: string + + constructor(type: string, name: string, path: string) { + this.name = name + this.type = type + this.path = path + } + + isLeaf() { + return true + } + + getUri(base: Uri): Uri { + return base.with({ path: this.path + "/source/main" }) + } + namespace(): string { + return this.name.match(/^\//) + ? this.name.replace(/^\/([^\/]*)\/.*/, "$1") + : "" + } + nameinns(): string { + return this.name.replace(/^\/[^\/]*\/(.*)/, "$1") + } +} diff --git a/src/abap/AbapObjectFactory.ts b/src/abap/AbapObjectFactory.ts new file mode 100644 index 0000000..dc33c07 --- /dev/null +++ b/src/abap/AbapObjectFactory.ts @@ -0,0 +1,23 @@ +import { ObjectNode } from "../adt/AdtParser" +import { AbapObject } from "./AbapObject" +import { AbapPackage } from "./AbapPackage" +import { AbapFunctionGroup } from "./AbapFunctionGroup" +import { AbapSimpleObject } from "./AbapSimpleObject" + +export function fromObjectNode(node: ObjectNode): AbapObject { + let objtype = AbapObject + switch (node.OBJECT_TYPE) { + case "DEVC/K": + objtype = AbapPackage + break + case "FUGR/F": + objtype = AbapFunctionGroup + break + case "TABL/DT": + case "DOMA/DT": + case "DTEL/DE": + objtype = AbapSimpleObject + break + } + return new objtype(node.OBJECT_TYPE, node.OBJECT_NAME, node.OBJECT_URI) +} diff --git a/src/abap/AbapPackage.ts b/src/abap/AbapPackage.ts new file mode 100644 index 0000000..8f82f53 --- /dev/null +++ b/src/abap/AbapPackage.ts @@ -0,0 +1,16 @@ +import { AbapObject } from "./AbapObject" +import { Uri } from "vscode" + +export class AbapPackage extends AbapObject { + isLeaf() { + return false + } + getUri(base: Uri): Uri { + const ptype = encodeURIComponent(this.type) + const pname = encodeURIComponent(this.name) + return base.with({ + path: "/sap/bc/adt/repository/nodestructure", + query: `parent_name=${pname}&parent_tech_name=${pname}&parent_type=${ptype}&withShortDescriptions=true` + }) + } +} diff --git a/src/abap/AbapSimpleObject.ts b/src/abap/AbapSimpleObject.ts new file mode 100644 index 0000000..e58acdf --- /dev/null +++ b/src/abap/AbapSimpleObject.ts @@ -0,0 +1,8 @@ +import { AbapObject } from "./AbapObject" +import { Uri } from "vscode" + +export class AbapSimpleObject extends AbapObject { + getUri(base: Uri): Uri { + return base.with({ path: this.path }) + } +} diff --git a/src/abapFsProvider.ts b/src/abapFsProvider.ts index 26a48f0..57252e2 100644 --- a/src/abapFsProvider.ts +++ b/src/abapFsProvider.ts @@ -1,14 +1,11 @@ import * as vscode from "vscode" import { AdtPathManager } from "./adt/AdtPathManager" -import { AdtNode } from "./adt/AdtNode" export class AbapFsProvider implements vscode.FileSystemProvider { private _pathManager = new AdtPathManager() private _eventEmitter = new vscode.EventEmitter() readonly onDidChangeFile: vscode.Event = this ._eventEmitter.event - rooturl: string = "" - root: AdtNode = new AdtNode("") watch( uri: vscode.Uri, options: { recursive: boolean; excludes: string[] } @@ -16,41 +13,26 @@ export class AbapFsProvider implements vscode.FileSystemProvider { throw new Error("Method not implemented.") } stat(uri: vscode.Uri): vscode.FileStat | Thenable { - const uristring = uri.toString() - if (this.rooturl === "") this.rooturl = uristring - if (this.rooturl === uristring) { - const newroot = this._pathManager - .fetchDirectory(uristring) - .then(newroot => (this.root = newroot)) - return newroot - } - throw new Error("not found") + return this._pathManager.fetchFileOrDir(uri) } readDirectory( uri: vscode.Uri ): [string, vscode.FileType][] | Thenable<[string, vscode.FileType][]> { - if (uri.toString() !== this.rooturl || !this.root) { - throw new Error("Only root directory for now...") - } const result: [string, vscode.FileType][] = [] - Array.from(this.root.entries).forEach(([key, value]) => - result.push([key, value.type]) - ) + const dir = this._pathManager.getDirectory(uri) + if (dir) + Array.from(dir.entries).forEach(([key, value]) => + result.push([key.replace(/\//g, "_"), value.type]) + ) return result } createDirectory(uri: vscode.Uri): void | Thenable { throw new Error("Method not implemented.") } readFile(uri: vscode.Uri): Uint8Array | Thenable { - // if (uri.path === "/dummy.abap" && this.root) { - // return this.root.then(x => { - // const child = x.entries.get("dummy.abap") - // if (child && child instanceof AdtFile && child.data) { - // return child.data - // } - // }) - // } - throw new Error("Method not implemented.") + const file = this._pathManager.find(uri) + if (file && file.body) return file.body + return new Uint8Array([]) } writeFile( uri: vscode.Uri, diff --git a/src/adt/AdtConnection.ts b/src/adt/AdtConnection.ts index ed93d24..3651bf8 100644 --- a/src/adt/AdtConnection.ts +++ b/src/adt/AdtConnection.ts @@ -1,4 +1,7 @@ import * as request from "request" +// import { AdtPathClassifier } from "./AdtPathClassifier" +import { Uri } from "vscode" + enum ConnStatus { new, active, @@ -19,6 +22,7 @@ export class AdtConnection { this.username = username this.password = password } + isActive(): boolean { return this._status === ConnStatus.active } @@ -42,14 +46,9 @@ export class AdtConnection { }) } - request( - path: string, - method: string = "GET", - config: request.Options | Object = {} - ): Promise { - let relativePath = path.replace(/(?:adt:\/)?\/[^\/]*\/sap\/bc\/adt/i, "") - const request = this.createrequest(relativePath, method, config) - return this.myrequest(request) + request(uri: Uri, method: string): Promise { + const path = uri.query ? uri.path + "?" + uri.query : uri.path + return this.myrequest(this.createrequest(path, method)) } private createrequest( @@ -67,9 +66,10 @@ export class AdtConnection { }, method, headers: { - "x-csrf-token": this._csrftoken + "x-csrf-token": this._csrftoken, + Accept: "*/*" } - } + } as request.Options //workaround for compiler bug } private myrequest(options: request.Options): Promise { return new Promise((resolve, reject) => { diff --git a/src/adt/AdtFile.ts b/src/adt/AdtFile.ts deleted file mode 100644 index 14b7837..0000000 --- a/src/adt/AdtFile.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FileStat, FileType } from "vscode" - -export class AdtFile implements FileStat { - type: FileType = FileType.File - name: string - ctime: number - mtime: number - size: number = 0 - data: any - constructor( - name: string, - ctime: number = Date.now(), - mtime: number = Date.now() - ) { - this.name = name - this.ctime = ctime - this.mtime = mtime - } -} diff --git a/src/adt/AdtNode.ts b/src/adt/AdtNode.ts index cdabffb..15a6919 100644 --- a/src/adt/AdtNode.ts +++ b/src/adt/AdtNode.ts @@ -1,40 +1,33 @@ -import { FileStat, FileType } from "vscode" -import { AdtFile } from "./AdtFile" -import { ObjectNode } from "./AdtParser" - -export type AdtDirItem = AdtFile | AdtNode +import { FileStat, FileType, Uri } from "vscode" export class AdtNode implements FileStat { - static fromTreeContent(fromTreeContent: ObjectNode[]): AdtNode { - const node = new AdtNode("") - return node - } - type: FileType = FileType.Directory - name: string + type: FileType ctime: number mtime: number size: number = 0 - entries: Map - constructor( - name: string, - ctime: number = Date.now(), - mtime: number = Date.now() - ) { - this.name = name - this.ctime = ctime - this.mtime = mtime + entries: Map + uri: Uri + fetched: boolean + body: Buffer | undefined + needRefresh(): any { + return !this.fetched + } + constructor(path: Uri, isDirectory: boolean, fetched: boolean) { + this.ctime = Date.now() + this.mtime = Date.now() this.entries = new Map() + this.uri = path + this.type = isDirectory ? FileType.Directory : FileType.File + this.fetched = fetched + } + + childPath(childname: string): string { + const sep = this.uri.path.match(/\/$/) || childname.match(/^\//) ? "" : "/" + return this.uri.path + sep + childname } - setChildrenFromTreeContent(children: ObjectNode[]): AdtNode { - this.entries.clear() - children.forEach(objnode => { - this.entries.set( - objnode.OBJECT_NAME, - objnode.EXPANDABLE - ? new AdtNode(objnode.OBJECT_NAME) - : new AdtFile(objnode.OBJECT_NAME) - ) - }) - return this + setContents(body: string): void { + this.body = Buffer.from(body) + this.size = this.body.length + this.fetched = true } } diff --git a/src/adt/AdtParser.ts b/src/adt/AdtParser.ts index 5c06889..e20d5f0 100644 --- a/src/adt/AdtParser.ts +++ b/src/adt/AdtParser.ts @@ -52,12 +52,12 @@ export const getNode = (...args: any[]) => { ) return [functions, rest] } - const fn = (...fargs: any) => { + const fn = (...fargs: any[]) => { const [functions, rest] = split(...fargs) if (functions.length === 0) return rest[0] const piped = pipe(...functions) return rest.length === 0 - ? (...iargs: any) => fn(piped, ...iargs) + ? (...iargs: any[]) => fn(piped, ...iargs) : piped(...rest) } return fn(...args) diff --git a/src/adt/AdtPathManager.ts b/src/adt/AdtPathManager.ts index f3c4e9e..99c99c8 100644 --- a/src/adt/AdtPathManager.ts +++ b/src/adt/AdtPathManager.ts @@ -1,45 +1,68 @@ import { AdtNode } from "./AdtNode" -import { adtPathResolver, AdtPath } from "./adtPathResolver" -import { AdtConnectionManager } from "./AdtConnectionManager" import { Response } from "request" import { getNodeStructureTreeContent, ObjectNode } from "./AdtParser" +import { getServer, AdtServer } from "./AdtServer" +import { fromObjectNode } from "../abap/AbapObjectFactory" +import { Uri, FileSystemError, FileType } from "vscode" export class AdtPathManager { - private _cache: Map = new Map() - private _manager = AdtConnectionManager.getManager() - parse(path: AdtPath, response: Response): any { - return getNodeStructureTreeContent(response.body).then( - (children: ObjectNode[]) => { - const node = new AdtNode(path.url) - node.setChildrenFromTreeContent(children) - return node - } - ) + getDirectory(uri: Uri): AdtNode | undefined { + return getServer(uri.authority).getDirectory(uri.path) + } + find(uri: Uri): AdtNode | undefined { + const server = getServer(uri.authority) + let node = server.getDirectory(uri.path) + if (node) return node + const matches = uri.path.match(/(.*)\/([^\/]+)$/) + if (matches) { + const [dir, name] = matches.slice(1) + let parent = server.getDirectory(dir) + let node = parent && parent.entries.get(name) + if (node) return node + } } - fetchDirectory(url: string): Promise { - let path = adtPathResolver(url) - return new Promise((resolve, reject) => { - if (path) { - let key = path!.connectionName + path!.path - let response = this._cache.get(key) - if (response) { - resolve(response) - } else { - this._manager.findConn(path.connectionName).then(conn => { - conn - .request(path!.path, path!.method) - .then(response => { - return this.parse(path!, response) - }) - .then(file => { - this._cache.set(key, file) - resolve(file) - }) - }) + + parse( + uri: Uri, + response: Response, + server: AdtServer, + node: AdtNode | undefined + ): Promise | AdtNode { + if ( + response.request.uri.path && + response.request.uri.path.match(/\/nodestructure/i) + ) + return getNodeStructureTreeContent(response.body).then( + (children: ObjectNode[]) => { + if (node) node.entries.clear() + else node = new AdtNode(uri, true, true) + server.addNodes(node, children.map(fromObjectNode)) + node.fetched = true + return node } - } else { - reject() - } - }) + ) + else if (node && node.type === FileType.File) { + node.setContents(response.body) + return node + } + throw FileSystemError.FileNotFound(uri.path) + } + + fetchFileOrDir(vsUrl: Uri): Promise | AdtNode { + const server = getServer(vsUrl.authority) + + const cached = this.find(vsUrl) + if (cached && !cached.needRefresh()) { + return cached + } + + const url = server.actualUri(vsUrl) + + return server.connectionP + .then(conn => conn.request(url, this.getMethod(url))) + .then(response => this.parse(vsUrl, response, server, cached)) + } + getMethod(uri: Uri): string { + return uri.path.match(/\/nodestructure/i) ? "POST" : "GET" } } diff --git a/src/adt/AdtServer.ts b/src/adt/AdtServer.ts new file mode 100644 index 0000000..455cc32 --- /dev/null +++ b/src/adt/AdtServer.ts @@ -0,0 +1,100 @@ +import { AdtConnectionManager } from "./AdtConnectionManager" +import { AdtConnection } from "./AdtConnection" +import { AdtNode } from "./AdtNode" +import { Uri, FileSystemError, FileType } from "vscode" +import { AbapObject } from "../abap/AbapObject" +// visual studio paths are hierarchic, adt ones aren't +// so we need a way to translate the hierarchic ones to the original ones +// this file is concerned with telling whether a path is a real ADT one or one from vscode +// /sap/bc/adt/repository/nodestructure (with ampty query) is the root of both +// also, several objects have namespaces. +// Class /foo/bar of package /foo/baz in code will have a path like +// /sap/bc/adt/repository/nodestructure/foo/baz/foo/bar +// the actual adt path would be something like: +// /sap/bc/adt/oo/classes/%2Ffoo%2Fbar +// so we need to do quite a bit of transcoding +const isValid = (vsUri: Uri): boolean => { + const matches = vsUri.path.match( + /^\/sap\/bc\/adt\/repository\/nodestructure\/?(.*)/i + ) + return !!(matches && !matches[1].match(/^\./)) +} +export class AdtServer { + readonly connectionId: string + readonly connectionP: Promise + private directories: Map = new Map() + private objectUris: Map = new Map() + + private addChildrenToNs(node: AdtNode, objects: AbapObject[]) { + objects.forEach(object => { + const childname = node.childPath(object.nameinns()) + const child = new AdtNode( + node.uri.with({ path: childname }), + !object.isLeaf(), + false + ) + node.entries.set(object.nameinns(), child) + this.objectUris.set(childname, object.getUri(node.uri)) + if(child.type=== FileType.Directory)this.directories.set(childname,child) + }) + } + + actualUri(original: Uri): Uri { + if (!isValid(original)) throw FileSystemError.FileNotFound(original) + return this.objectUris.get(original.path) || original + } + + addNodes(parent: AdtNode, objects: AbapObject[]) { + this.directories.set(parent.uri.path, parent) + const namespaces = objects.reduce((map, obj) => { + const nsname = obj.namespace() + let ns = map.get(nsname) + if (!ns) { + ns = [] + map.set(nsname, ns) + } + ns.push(obj) + return map + }, new Map()) + + //for every namespace create a node, add the children to it + // so package /foo/bar will be rendered in + // a namespace folder foo + // with a package bar inside + namespaces.forEach((objects, name) => { + if (name !== "") { + const nodeName = parent.childPath(name) + const node = new AdtNode( + parent.uri.with({ path: nodeName }), + true, + true + ) + parent.entries.set(name, node) + this.addChildrenToNs(node, objects) + this.directories.set(nodeName, node) + } + }) + //add objects without a namespace + namespaces.forEach((objects, name) => { + if (name === "") this.addChildrenToNs(parent, objects) + }) + } + + getDirectory(name: string): AdtNode | undefined { + return this.directories.get(name) + } + + constructor(connectionId: string) { + this.connectionId = connectionId + this.connectionP = AdtConnectionManager.getManager().findConn(connectionId) + } +} +const servers = new Map() +export const getServer = (connId: string): AdtServer => { + let server = servers.get(connId) + if (!server) { + server = new AdtServer(connId) + servers.set(connId, server) + } + return server +} diff --git a/src/adt/adtPathResolver.ts b/src/adt/adtPathResolver.ts deleted file mode 100644 index b8a4883..0000000 --- a/src/adt/adtPathResolver.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface AdtPath { - method: string - path: string - connectionName: string - isFolder: boolean - url: string -} -export const adtPathResolver = (url: string): AdtPath | undefined => { - const match = url.match(/adt:\/\/([^\/]+)\/sap\/bc\/adt(.*)/i) - if (match) { - let method = "GET" - let isFolder = true - let [connectionName, path] = match.splice(1) - if (path.match(/^(\/?|(\/repository(\/?nodestructure)?))$/i)) { - //partial root - path = "/sap/bc/adt/repository/nodestructure" - method = "POST" - } else if (!path.match(/\/(.*)\//)) { - throw new Error("Not found") - } - return { method, path, connectionName, isFolder, url } - } -} diff --git a/src/config.ts b/src/config.ts index 7b6c574..59f4f12 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,13 +1,22 @@ import * as vscode from "vscode" +export interface RemoteConfig { + name: string + url: string + username: string + password: string +} -const config = (name: string, remote: any) => { +const config = (name: string, remote: RemoteConfig) => { const conf = { url: "", ...remote, name, valid: true } conf.valid = !!(remote.url && remote.username && remote.password) return conf } -export function getRemoteList() { +export function getRemoteList(): RemoteConfig[] { const userConfig = vscode.workspace.getConfiguration("abapfs") const remote = userConfig.remote - return Object.keys(remote).map(name => config(name, remote[name])) + if (!remote) throw new Error("No destination configured") + return Object.keys(remote).map(name => + config(name, remote[name] as RemoteConfig) + ) } diff --git a/src/extension.ts b/src/extension.ts index cab7b7f..9d74894 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,13 @@ "use strict" import * as vscode from "vscode" import { AbapFsProvider } from "./abapFsProvider" -import { getRemoteList } from "./config" +import { getRemoteList, RemoteConfig } from "./config" import { AdtConnectionManager } from "./adt/AdtConnectionManager" -function selectRemote() { +function selectRemote(connection: string): Thenable { const remotes = getRemoteList() + if (remotes[1] && remotes[1].name === connection) + return new Promise(resolve => resolve(remotes[1])) return vscode.window .showQuickPick( remotes.map(remote => ({ @@ -17,7 +19,10 @@ function selectRemote() { placeHolder: "Please choose a remote" } ) - .then(selection => selection && selection.remote) + .then(selection => { + if (selection) return selection.remote + throw new Error("No connection selected") + }) } export function activate(context: vscode.ExtensionContext) { @@ -28,20 +33,28 @@ export function activate(context: vscode.ExtensionContext) { }) ) - let disposable = vscode.commands.registerCommand("abapfs.connect", () => { - selectRemote().then(remote => { - return AdtConnectionManager.getManager() - .setConn(remote) - .then(() => { - if (remote) { - vscode.workspace.updateWorkspaceFolders(0, 0, { - uri: vscode.Uri.parse("adt://" + remote.name + "/sap/bc/adt/"), - name: "ABAP" - }) - } - }) - }) - }) + let disposable = vscode.commands.registerCommand( + "abapfs.connect", + (selector: any) => { + const connection = selector && selector.connection + selectRemote(connection).then(remote => { + return AdtConnectionManager.getManager() + .setConn(remote) + .then(() => { + if (remote) { + vscode.workspace.updateWorkspaceFolders(0, 0, { + uri: vscode.Uri.parse( + "adt://" + + remote.name + + "/sap/bc/adt/repository/nodestructure" + ), + name: remote.name + "(ABAP)" + }) + } + }) + }) + } + ) context.subscriptions.push(disposable) }