Skip to content

Commit

Permalink
Fix lsp snippet escapes
Browse files Browse the repository at this point in the history
  • Loading branch information
aslakhellesoy committed Apr 25, 2022
1 parent 3ab3ded commit e9930f2
Show file tree
Hide file tree
Showing 9 changed files with 52 additions and 18 deletions.
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
"files": [
"dist/cjs",
"dist/esm",
"*.wasm"
"dist/*.wasm"
],
"module": "dist/esm/src/index.js",
"jsnext:main": "dist/esm/src/index.js",
"exports": {
".": {
"import": "./dist/esm/src/index.js",
"require": "./dist/cjs/src/index.js"
},
"./node": {
"import": "./dist/esm/src/tree-sitter-node/NodeParserAdapter.js",
"require": "./dist/cjs/src/tree-sitter-node/NodeParserAdapter.js"
},
"./wasm": {
"import": "./dist/esm/src/tree-sitter-wasm/WasmParserAdapter.js",
"require": "./dist/cjs/src/tree-sitter-wasm/WasmParserAdapter.js"
}
},
"scripts": {
Expand All @@ -30,7 +38,7 @@
"upgrade": "npm-check-updates --upgrade",
"prepare": "husky install",
"pretty-quick-staged": "pretty-quick --staged",
"postinstall": "scripts/build.js"
"postinstall": "scripts/build.js && cp node_modules/web-tree-sitter/tree-sitter.wasm dist"
},
"repository": {
"type": "git",
Expand Down
7 changes: 5 additions & 2 deletions src/service/snippet/lspCompletionSnippet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Generates an [LSP Completion Snippet]{@link https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax}
* Generates an [LSP Completion Snippet]{@link https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax}
*
* @param expression the expression to generate the snippet from
*/
Expand All @@ -13,5 +13,8 @@ export function lspCompletionSnippet(segments: SuggestionSegments): string {
}

function lspPlaceholder(i: number, choices: readonly string[]) {
return `\${${i}|${choices.join(',')}|}`
const escapedChoices = choices
.filter((choice) => choice !== '')
.map((choice) => choice.replace(/([$\\},|])/g, '\\$1'))
return `\${${i}|${escapedChoices.join(',')}|}`
}
17 changes: 16 additions & 1 deletion src/suggestions/buildSuggestionFromCucumberExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,22 @@ function compileExpression(
registry: ParameterTypeRegistry,
parameterChoices: ParameterChoices
): CompileResult {
return (node.nodes || []).map((node) => compile(node, registry, parameterChoices))
return (node.nodes || []).reduce<CompileResult[]>((prev, curr) => {
const child = compile(curr, registry, parameterChoices)
// If we have an optional, we'll create two choice segments - one with and one without the optional
if (curr.type === NodeType.optional) {
const last = prev[prev.length - 1]
if (!(typeof last === 'string')) {
throw new Error(`Expected a string, but was ${JSON.stringify(last)}`)
}
if (!Array.isArray(child)) {
throw new Error(`Expected an array, but was ${JSON.stringify(child)}`)
}
return prev.slice(0, prev.length - 1).concat([[last, last + child[0]]])
} else {
return prev.concat([child])
}
}, [])
}

function defaultParameterChoices(parameterType: ParameterType<unknown>): readonly string[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Java from 'tree-sitter-java'
// @ts-ignore
import TypeScript from 'tree-sitter-typescript'

import { LanguageName, ParserAdapter } from './types'
import { LanguageName, ParserAdapter } from '../tree-sitter/types'

export class NodeParserAdapter implements ParserAdapter {
readonly parser = new Parser()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import NodeParser from 'tree-sitter'
import Parser from 'web-tree-sitter'

import { LanguageName, LanguageNames, ParserAdapter } from './types.js'
import { LanguageName, LanguageNames, ParserAdapter } from '../tree-sitter/types.js'

export class WasmParserAdapter implements ParserAdapter {
// @ts-ignore
public parser: Parser
public parser: NodeParser
private languages: Record<LanguageName, Parser.Language>

async init(wasmBaseUrl: string) {
await Parser.init()
// @ts-ignore
this.parser = new Parser()

const languages = await Promise.all(
Expand All @@ -23,8 +24,7 @@ export class WasmParserAdapter implements ParserAdapter {
)
}

// @ts-ignore
query(source: string): Parser.Query {
query(source: string): NodeParser.Query {
return this.parser.getLanguage().query(source)
}

Expand Down
1 change: 0 additions & 1 deletion src/tree-sitter/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './ExpressionBuilder.js'
export * from './NodeParserAdapter.js'
export * from './types.js'
12 changes: 11 additions & 1 deletion test/service/snippet/lspCompletionSnippet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { lspCompletionSnippet } from '../../../src/service/snippet/lspCompletion
import { SuggestionSegments } from '../../../src/suggestions/types.js'

describe('lspCompletionSnippet', () => {
it('converts StepSegments to an LSP snippet', () => {
it('converts segments to an LSP snippet', () => {
const segments: SuggestionSegments = [
'I have ',
['42', '54'],
Expand All @@ -16,4 +16,14 @@ describe('lspCompletionSnippet', () => {
'I have ${1|42,54|} cukes in my ${2|basket,belly,table|}'
)
})

it('removes empty suggestions', () => {
const segments: SuggestionSegments = ['I have cuke', ['s', '']]
assert.strictEqual(lspCompletionSnippet(segments), 'I have cuke${1|s|}')
})

it('escapes special characters', () => {
const segments: SuggestionSegments = ['the choices are ', ['', '$', '\\', '}', ',', '|']]
assert.strictEqual(lspCompletionSnippet(segments), 'the choices are ${1|\\$,\\\\,\\},\\,,\\||}')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('buildSuggestionFromCucumberExpression', () => {
it('builds an item from optional expression', () => {
const expression = new CucumberExpression('I have 1 cuke(s)', registry)
const expected: Suggestion = {
segments: ['I have 1 cuke', ['s', '']],
segments: ['I have 1 ', ['cuke', 'cukes']],
label: 'I have 1 cuke(s)',
}
const actual = buildSuggestionFromCucumberExpression(expression, registry, {})
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('buildSuggestionFromCucumberExpression', () => {
it('builds an item from complex expression', () => {
const expression = new CucumberExpression('I have {int} cuke(s) in my bag/belly', registry)
const expected: Suggestion = {
segments: ['I have ', ['12'], ' cuke', ['s', ''], ' in my ', ['bag', 'belly']],
segments: ['I have ', ['12'], ' ', ['cuke', 'cukes'], ' in my ', ['bag', 'belly']],
label: 'I have {int} cuke(s) in my bag/belly',
}
const actual = buildSuggestionFromCucumberExpression(expression, registry, {
Expand Down
5 changes: 2 additions & 3 deletions test/tree-sitter/ExpressionBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import glob from 'glob'
import path from 'path'

import { ExpressionBuilder, LanguageName } from '../../src/index.js'
import { NodeParserAdapter } from '../../src/tree-sitter/NodeParserAdapter.js'
import { ParserAdapter } from '../../src/tree-sitter/types'
import { WasmParserAdapter } from '../../src/tree-sitter/WasmParserAdapter.js'
import { NodeParserAdapter } from '../../src/tree-sitter-node/NodeParserAdapter.js'
import { WasmParserAdapter } from '../../src/tree-sitter-wasm/WasmParserAdapter.js'

function defineContract(makeParserAdapter: () => Promise<ParserAdapter>) {
let expressionBuilder: ExpressionBuilder
Expand Down Expand Up @@ -41,7 +41,6 @@ describe('ExpressionBuilder', () => {
})

context('with WasmParserAdapter', () => {
// @ts-ignore
defineContract(async () => {
const wasmParserAdapter = new WasmParserAdapter()
await wasmParserAdapter.init('dist')
Expand Down

0 comments on commit e9930f2

Please sign in to comment.