Skip to content

Commit

Permalink
Merge branch 'write-support'
Browse files Browse the repository at this point in the history
  • Loading branch information
marcellourbani committed Oct 31, 2018
2 parents efb5fb3 + e781bd9 commit a1d9e12
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 185 deletions.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Early stages, for now it only displays ABAP code for programs, function groups, classes and few others (actual list depends on ADT version)
This extension allows editing of ABAP code on your server directly in Visual studio code.
It's still in its early stages

<aside class="warning">
WRITE SUPPORT IS EXPERIMANTAL USE AT YOUR OWN RISK
</aside>

![anim](https://user-images.githubusercontent.com/2453277/47482169-ae0cc300-d82d-11e8-8d19-f55dd877c166.gif)
![image](https://user-images.githubusercontent.com/2453277/47466602-dd99dc00-d7e9-11e8-97ed-28e23dfd8f90.png)
Expand All @@ -12,12 +16,21 @@ Sadly [ABAPlint](https://marketplace.visualstudio.com/items?itemName=larshp.vsco
## Features

Connect to your SAP server using the ADT interface
The complete list of editable objects depends on your installation, on my local 7.51 works for:

- programs/includes
- function groups
- classes
- transformations
<aside class="warning">
Saved objects will be deactivated, this doesn't allow activation yet
</aside>

## setup

Too early to publish as an extension, there's a compiled extension you can run from source or install from the command line with

`code --install-extension vscode-abap-remote-fs-0.0.2.vsix`
`code --install-extension vscode-abap-remote-fs-0.0.3.vsix`

Once installed you'll need an ABAP system with the ADT (Abap Developer Tools for eclipse) installed and SICF node `/sap/bc/adt` activated:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-abap-remote-fs",
"displayName": "vscode_abap_remote_fs",
"description": "Work on your ABAP code straight from the server",
"version": "0.0.2",
"version": "0.0.3",
"publisher": "murbani",
"author": {
"email": "[email protected]",
Expand Down
24 changes: 0 additions & 24 deletions src/abap/AbapClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,4 @@ export class AbapClass extends AbapObject {
)
.then(aggregateNodes)
}

// protected filterNodeStructure(nodest: NodeStructure): NodeStructure {
// const nodes = nodest.nodes.filter(
// x =>
// this.whiteListed(x.OBJECT_TYPE) &&
// (x.OBJECT_TYPE !== "PROG/I" || //keep includes only if they start with the program name
// x.OBJECT_NAME.length === this.name.length + 3) &&
// x.OBJECT_NAME.substr(0, this.name.length) === this.name
// )

// nodes.unshift({
// OBJECT_NAME: this.name,
// OBJECT_TYPE: this.type,
// OBJECT_URI: this.path,
// OBJECT_VIT_URI: this.path,
// EXPANDABLE: "",
// TECH_NAME: this.techName
// })

// return {
// ...nodest,
// nodes
// }
// }
}
86 changes: 70 additions & 16 deletions src/abap/AbapObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
objectVersion
} from "../adt/AdtObjectParser"
import { aggregateNodes } from "./AbapObjectUtilities"
import { adtLockParser } from "../adt/AdtLockParser"

export const XML_EXTENSION = ".XML"
export const SAPGUIONLY = "Objects of this type are only supported in SAPGUI"
Expand Down Expand Up @@ -47,7 +48,9 @@ export class AbapObject {
this.path = path
this.expandable = !!expandable
this.techName = techName || name
this.sapguiOnly = !!path.match(/\/sap\/bc\/adt\/vit/)
this.sapguiOnly = !!path.match(
"(/sap/bc/adt/vit)|(/sap/bc/adt/ddic/domains/)|(/sap/bc/adt/ddic/dataelements/)"
)
}

isLeaf() {
Expand All @@ -63,35 +66,86 @@ export class AbapObject {
})
}

getContents(connection: AdtConnection): Promise<string> {
async setContents(
connection: AdtConnection,
contents: Uint8Array
): Promise<void> {
if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName())
if (this.sapguiOnly) return Promise.resolve(SAPGUIONLY)
if (this.sapguiOnly)
throw FileSystemError.FileNotFound(
`${this.name} can only be edited in SAPGUI`
)
let baseUri = await this.getFileUri(connection)
baseUri = baseUri.with({ path: baseUri.path.replace(/\?.*/, "") })

const lockRecord = await connection
.request(
baseUri.with({ query: "_action=LOCK&accessMode=MODIFY" }),
"POST",
{ headers: { "X-sap-adt-sessiontype": "stateful" } }
)
.then(pick("body"))
.then(parsetoPromise() as any)
.then(adtLockParser)

const lock = encodeURI(lockRecord.LOCK_HANDLE)
console.log(lock, lockRecord)

await connection.request(
baseUri.with({ query: `lockHandle=${lock}` }),
"PUT",
{ body: contents }
)

await connection.request(
baseUri.with({ query: `_action=UNLOCK&lockHandle=${lock}` }),
"POST"
)
console.log("saved:" + baseUri.path)
}
async getFileUri(connection: AdtConnection): Promise<Uri> {
const mainUri = this.getUri(connection)

if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName())
if (this.sapguiOnly)
throw FileSystemError.Unavailable(this.vsName() + SAPGUIONLY)
//bit of heuristics: assume we're already dealing with a source file
// if source/main is part of the url. Won't get an XML file with the original anyway
// same for class includes
if (
this.path.match(/\/source\/main/) ||
this.path.match(/\/includes\/[a-zA-Z]+$/)
)
return connection.request(mainUri, "GET").then(pick("body"))
return mainUri.with({ path: this.path })

const follow = this.followLinkGen(mainUri)

return connection
const objectRecord = await connection
.request(mainUri, "GET")
.then(pick("body"))
.then(parsetoPromise())
.then(parseObject)
.then(o => {
const link = firstTextLink(o.links)
if (link) {
const query = objectVersion(o.header)
const actualUri = follow(link.href).with({ query })

return connection.request(actualUri, "GET").then(pick("body"))
} else return SAPGUIONLY
})
const link = firstTextLink(objectRecord.links)

if (link) {
const query = objectVersion(objectRecord.header)
return this.followLinkGen(mainUri)(link.href).with({ query })
}
return Promise.reject(
FileSystemError.Unavailable(this.vsName() + SAPGUIONLY)
)
}

async getContents(connection: AdtConnection): Promise<string> {
if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName())
if (this.sapguiOnly) return Promise.resolve(SAPGUIONLY)

let uri
try {
uri = await this.getFileUri(connection)
} catch (e) {
return Promise.reject(e)
}

return connection.request(uri, "GET").then(pick("body"))
}

getExtension(): any {
Expand Down
83 changes: 51 additions & 32 deletions src/adt/AdtConnection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as request from "request"
// import { AdtPathClassifier } from "./AdtPathClassifier"
import { Uri, FileSystemError } from "vscode"
import { Uri } from "vscode"
import { RemoteConfig } from "../config"

enum ConnStatus {
new,
Expand Down Expand Up @@ -46,18 +46,23 @@ export class AdtConnection {
})
}

request(uri: Uri, method: string): Promise<request.Response> {
request(
uri: Uri,
method: string,
config: request.Options | Object = {}
): Promise<request.Response> {
const path = uri.query ? uri.path + "?" + uri.query : uri.path
return this.myrequest(this.createrequest(path, method))
return this.myrequest(path, method, config)
}

private createrequest(
private myrequest(
path: string,
method: string = "GET",
config: request.Options | Object = {}
): request.Options {
return {
...config,
options: request.CoreOptions = {}
): Promise<request.Response> {
const { headers, ...rest } = options
const urlOptions: request.OptionsWithUrl = {
...rest,
url: this.url + path,
jar: true,
auth: {
Expand All @@ -66,44 +71,58 @@ export class AdtConnection {
},
method,
headers: {
...headers,
"x-csrf-token": this._csrftoken,
Accept: "*/*"
}
} as request.Options //workaround for compiler bug
}
private myrequest(options: request.Options): Promise<request.Response> {
}

return new Promise((resolve, reject) => {
request(options, (error, response, body) => {
if (error) throw error
if (response.statusCode < 300) resolve(response)
request(urlOptions, (error, response, body) => {
if (error) reject(error)
else if (response.statusCode < 300) resolve(response)
else
throw FileSystemError.NoPermissions(
`Failed to connect to ${this.name}:${response.statusCode}:${
response.statusMessage
}`
reject(
new Error(
`Failed to connect to ${this.name}:${response.statusCode}:${
response.statusMessage
}`
)
)
})
})
}

connect(): Promise<request.Response> {
return this.myrequest(
this.createrequest("/sap/bc/adt/compatibility/graph")
).then((response: request.Response) => {
const newtoken = response.headers["x-csrf-token"]
if (typeof newtoken === "string") {
this._csrftoken = newtoken
}
if (response.statusCode < 300) {
this.setStatus(ConnStatus.active)
} else {
this.setStatus(ConnStatus.failed)
return this.myrequest("/sap/bc/adt/compatibility/graph").then(
(response: request.Response) => {
const newtoken = response.headers["x-csrf-token"]
if (typeof newtoken === "string") {
this._csrftoken = newtoken
}
if (response.statusCode < 300) {
this.setStatus(ConnStatus.active)
} else {
this.setStatus(ConnStatus.failed)
}
return response
}
return response
})
)
}

setStatus(newStatus: ConnStatus): any {
this._status = newStatus
this._listeners.forEach(l => l())
}

static fromRemote(config: RemoteConfig) {
const connection = new AdtConnection(
config.name,
config.url,
config.username,
config.password
)

return connection
}
}
57 changes: 0 additions & 57 deletions src/adt/AdtConnectionManager.ts

This file was deleted.

17 changes: 17 additions & 0 deletions src/adt/AdtLockParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defaultVal, mapWidth } from "../functions"

import { getNode, recxml2js } from "./AdtParserBase"

interface AdtLock {
LOCK_HANDLE: string
CORRNR: string
CORRUSER: string
CORRTEXT: string
IS_LOCAL: string
IS_LINK_UP: string
MODIFICATION_SUPPORT: string
}
export const adtLockParser = defaultVal(
[],
getNode("asx:abap/asx:values/DATA", mapWidth(recxml2js), (x: any[]) => x[0])
) as (xml: string) => AdtLock
Loading

0 comments on commit a1d9e12

Please sign in to comment.