Skip to content

Add explainshell integration #45

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

Merged
merged 13 commits into from
Jun 6, 2018
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Bash Language Server

Bash language server implementation based on [Tree Sitter][tree-sitter] and its
[grammar for Bash][tree-sitter-bash].
[grammar for Bash][tree-sitter-bash] with [explainshell][explainshell] integration.

## Features

Expand All @@ -11,6 +11,7 @@ Bash language server implementation based on [Tree Sitter][tree-sitter] and its
- [x] Highlight occurrences
- [x] Code completion
- [x] Simple diagnostics reporting
- [x] Documentation for flags on hover
- [ ] Rename symbol

## Installation
Expand Down Expand Up @@ -44,3 +45,4 @@ Please see [docs/development-guide][dev-guide] for more information.
[vscode-marketplace]: https://marketplace.visualstudio.com/items?itemName=mads-hartmann.bash-ide-vscode
[dev-guide]: https://github.com/mads-hartmann/bash-language-server/blob/master/docs/development-guide.md
[ide-bash]: https://atom.io/packages/ide-bash
[explainshell]: https://explainshell.com/
3 changes: 3 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
},
"dependencies": {
"glob": "^7.1.2",
"request-promise-native": "^1.0.5",
"tree-sitter": "^0.11.0",
"tree-sitter-bash": "^0.11.0",
"turndown": "^4.0.2",
"urijs": "^1.19.1",
"vscode-languageserver": "^4.1.1"
},
"scripts": {
Expand Down
77 changes: 75 additions & 2 deletions server/src/analyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import * as fs from 'fs'
import * as glob from 'glob'
import * as Path from 'path'

import * as request from 'request-promise-native'
import { Document } from 'tree-sitter'
import * as bash from 'tree-sitter-bash'
import * as URI from 'urijs'
import * as LSP from 'vscode-languageserver'

import { uniqueBasedOnHash } from './util/array'
Expand Down Expand Up @@ -51,14 +53,19 @@ export default class Analyzer {
const absolute = Path.join(rootPath, p)
const uri = 'file://' + absolute
connection.console.log('Analyzing ' + uri)
analyzer.analyze(uri, fs.readFileSync(absolute, 'utf8'))
analyzer.analyze(
uri,
LSP.TextDocument.create(uri, 'shell', 1, fs.readFileSync(absolute, 'utf8')),
)
})
resolve(analyzer)
}
})
})
}

private uriToTextDocument: { [uri: string]: LSP.TextDocument } = {}

private uriToTreeSitterDocument: Documents = {}

// We need this to find the word at a given point etc.
Expand All @@ -84,6 +91,69 @@ export default class Analyzer {
return symbols.map(s => s.location)
}

public async getExplainshellDocumentation({
pos,
endpoint,
}: {
pos: LSP.TextDocumentPositionParams
endpoint: string
}): Promise<any> {
const leafNode = this.uriToTreeSitterDocument[
pos.textDocument.uri
].rootNode.descendantForPosition({
row: pos.position.line,
column: pos.position.character,
})

// explainshell needs the whole command, not just the "word" (tree-sitter
// parlance) that the user hovered over. A relatively successful heuristic
// is to simply go up one level in the AST. If you go up too far, you'll
// start to include newlines, and explainshell completely balks when it
// encounters newlines.
const interestingNode = leafNode.type === 'word' ? leafNode.parent : leafNode

const cmd = this.uriToFileContent[pos.textDocument.uri].slice(
interestingNode.startIndex,
interestingNode.endIndex,
)

const explainshellResponse = await request({
uri: URI(endpoint)
.path('/api/explain')
.addQuery('cmd', cmd)
.toString(),
json: true,
})

// Attaches debugging information to the return value (useful for logging to
// VS Code output).
const response = { ...explainshellResponse, cmd, cmdType: interestingNode.type }

if (explainshellResponse.status === 'error') {
return response
} else if (!explainshellResponse.matches) {
return { ...response, status: 'error' }
} else {
const offsetOfMousePointerInCommand =
this.uriToTextDocument[pos.textDocument.uri].offsetAt(pos.position) -
interestingNode.startIndex

const match = explainshellResponse.matches.find(
helpItem =>
helpItem.start <= offsetOfMousePointerInCommand &&
offsetOfMousePointerInCommand < helpItem.end,
)

const helpHTML = match && match.helpHTML

if (!helpHTML) {
return { ...response, status: 'error' }
}

return { ...response, helpHTML }
}
}

/**
* Find all the locations where something named name has been defined.
*/
Expand Down Expand Up @@ -157,12 +227,15 @@ export default class Analyzer {
* Returns all, if any, syntax errors that occurred while parsing the file.
*
*/
public analyze(uri: string, contents: string): LSP.Diagnostic[] {
public analyze(uri: string, document: LSP.TextDocument): LSP.Diagnostic[] {
const contents = document.getText()

const d = new Document()
d.setLanguage(bash)
d.setInputString(contents)
d.parse()

this.uriToTextDocument[uri] = document
this.uriToTreeSitterDocument[uri] = d
this.uriToDeclarations[uri] = {}
this.uriToFileContent[uri] = contents
Expand Down
26 changes: 23 additions & 3 deletions server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as LSP from 'vscode-languageserver'

import * as TurndownService from 'turndown'
import Analyzer from './analyser'
import * as Builtins from './builtins'
import Executables from './executables'
Expand Down Expand Up @@ -53,8 +54,7 @@ export default class BashServer {
this.documents.listen(this.connection)
this.documents.onDidChangeContent(change => {
const uri = change.document.uri
const contents = change.document.getText()
const diagnostics = this.analyzer.analyze(uri, contents)
const diagnostics = this.analyzer.analyze(uri, change.document)
connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
Expand Down Expand Up @@ -100,7 +100,7 @@ export default class BashServer {
)
}

private onHover(pos: LSP.TextDocumentPositionParams): Promise<LSP.Hover> {
private async onHover(pos: LSP.TextDocumentPositionParams): Promise<LSP.Hover> {
this.connection.console.log(
`Hovering over ${pos.position.line}:${pos.position.character}`,
)
Expand All @@ -121,6 +121,26 @@ export default class BashServer {
value: doc,
},
}))
} else if (process.env.EXPLAINSHELL_ENDPOINT !== '') {
const response = await this.analyzer.getExplainshellDocumentation({
pos,
endpoint: process.env.EXPLAINSHELL_ENDPOINT,
})

if (response.status === 'error') {
this.connection.console.log(
'getExplainshellDocumentation returned: ' + JSON.stringify(response, null, 4),
)

return null
}

return {
contents: {
kind: 'markdown',
value: new TurndownService().turndown(response.helpHTML),
},
}
} else {
return null
}
Expand Down
8 changes: 7 additions & 1 deletion vscode-client/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Bash IDE

Bash language server. Uses [Tree Sitter][tree-sitter] and its
[grammar for Bash][tree-sitter-bash].
[grammar for Bash][tree-sitter-bash] with [explainshell][explainshell] integration.

## System Requirements

Expand All @@ -20,7 +20,13 @@ npm i -g bash-language-server
- [x] Highlight occurrences
- [x] Code completion
- [x] Simple diagnostics reporting
- [x] Documentation for flags on hover
- [ ] Rename symbol

## Configuration

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we need some more explanation here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a link to how to set up explainshell in Docker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My internet is back up and I pushed the pre-built image https://hub.docker.com/r/chrismwendt/codeintel-bash-with-explainshell/ And I added this to the docs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. Maybe also a link to idank/explainshell#125 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

<lsp-adapter>

[tree-sitter]: https://github.com/tree-sitter/tree-sitter
[tree-sitter-bash]: https://github.com/tree-sitter/tree-sitter-bash
[explainshell]: https://explainshell.com/
14 changes: 13 additions & 1 deletion vscode-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,19 @@
"onLanguage:shellscript"
],
"main": "./out/src/extension",
"contributes": {},
"contributes": {
"configuration": {
"type": "object",
"title": "Bash IDE configuration",
"properties": {
"bash.explainshellEndpoint": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us call this bashIde. instead of bash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"type": "string",
"default": "",
"description": "Set this to https://explainshell.com (once https://github.com/idank/explainshell/pull/125 is merged in) in order to get hover documentation on flags and options."
}
}
}
},
"scripts": {
"vscode:prepublish": "yarn run compile",
"compile": "rm -rf out && tsc -p ./",
Expand Down
19 changes: 17 additions & 2 deletions vscode-client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,36 @@ export async function activate(context: ExtensionContext) {
if (semverCompare(version, MINIMUM_SERVER_VERSION) === -1) {
return handleOutdatedExecutable()
}
start(context, command)
start(
context,
command,
workspace.getConfiguration('bash').get('explainshellEndpoint', ''),
)
} catch (error) {
handleMissingExecutable()
}
}

function start(context: ExtensionContext, command: string) {
function start(context: ExtensionContext, command: string, explainshellEndpoint: string) {
const env: any = {
...process.env,
EXPLAINSHELL_ENDPOINT: explainshellEndpoint,
}

const serverOptions: ServerOptions = {
run: {
command,
args: ['start'],
options: {
env,
},
},
debug: {
command,
args: ['start'],
options: {
env,
},
},
}

Expand Down