diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2518ff72..6825e953 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -21,9 +21,9 @@ that are consumed by the Language Service API. | source code | | +----------+---------+ | | | -+----------v---------+ +-------v-------+ -| tree|sitter | | Gherkin Parser| -+----------+---------+ +-------+-------+ ++----------v---------+ +-------v--------+ +| tree-sitter | | Gherkin Parser | ++----------+---------+ +-------+--------+ | | +----------v---------+ +-------v-------+ | Expressions | | Gherkin Steps | @@ -31,13 +31,13 @@ that are consumed by the Language Service API. | | +-------------+-----------+ | - +---------v----------+ - | buildStepDocuments | - +---------+----------+ + +---------v--------+ + | buildSuggestions | + +---------+--------+ | - +-------v--------+ - | Step Documents | - +-------+--------+ + +------v------+ + | Suggestions | + +------+------+ | +-------v-------+ | Index | @@ -49,7 +49,7 @@ from Java/Ruby/TypeScript/JavaScript/etc. source code using [tree-sitter](https: Gherkin steps are extracted from Gherkin source code using the Gherkin parser. -Expressions and steps are passed to `buildStepDocuments` to produce an array of `StepDocument`, and these are used to update +Expressions and steps are passed to `buildSuggestions` to produce an array of `Suggestion`, and these are used to update a (search) `Index`. ## Language Service API diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ae3a8e..ba535754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Generate suggestions for Cucumber Expressions even if there are no matching steps. ([#16](https://github.com/cucumber/language-service/issues/16), [#32](https://github.com/cucumber/language-service/pull/32)) ### Changed +- Renamed `StepDocument` to `Suggestion` - The `ExpressionBuilder` constructor has changed. Consumers must provide a `ParserAdpater` - currently a `NodeParserAdapter` is the only implementation. ### Removed diff --git a/src/index.ts b/src/index.ts index a1ae33be..0f6bf787 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,5 @@ export * from './gherkin/index.js' export * from './index/index.js' export * from './messages/index.js' export * from './service/index.js' -export * from './step-documents/index.js' +export * from './suggestions/index.js' export * from './tree-sitter/index.js' diff --git a/src/index/bruteForceIndex.ts b/src/index/bruteForceIndex.ts index e8f738ec..7857ee0b 100644 --- a/src/index/bruteForceIndex.ts +++ b/src/index/bruteForceIndex.ts @@ -1,23 +1,21 @@ -import { StepDocument } from '../step-documents/types' +import { Suggestion } from '../suggestions/types.js' import { Index } from './types' /** * A brute force (not very performant or fuzzy-search capable) index that matches permutation expressions with string.includes() * - * @param stepDocuments + * @param suggestions */ -export function bruteForceIndex(stepDocuments: readonly StepDocument[]): Index { +export function bruteForceIndex(suggestions: readonly Suggestion[]): Index { return (text) => { if (!text) return [] const predicate = (segment: string) => segment.toLowerCase().includes(text.toLowerCase()) - return stepDocuments.filter((permutationExpression) => - matches(permutationExpression, predicate) - ) + return suggestions.filter((permutationExpression) => matches(permutationExpression, predicate)) } } -function matches(stepDocument: StepDocument, predicate: (segment: string) => boolean): boolean { - return !!stepDocument.segments.find((segment) => +function matches(suggestion: Suggestion, predicate: (segment: string) => boolean): boolean { + return !!suggestion.segments.find((segment) => typeof segment === 'string' ? predicate(segment) : !!segment.find(predicate) ) } diff --git a/src/index/fuseIndex.ts b/src/index/fuseIndex.ts index 843abef5..f1bfe317 100644 --- a/src/index/fuseIndex.ts +++ b/src/index/fuseIndex.ts @@ -1,16 +1,16 @@ import Fuse from 'fuse.js' -import { StepDocument } from '../step-documents/types.js' +import { Suggestion } from '../suggestions/types.js' import { Index } from './types.js' type Doc = { text: string } -export function fuseIndex(stepDocuments: readonly StepDocument[]): Index { - const docs: Doc[] = stepDocuments.map((stepDocument) => { +export function fuseIndex(suggestions: readonly Suggestion[]): Index { + const docs: Doc[] = suggestions.map((suggestion) => { return { - text: stepDocument.segments + text: suggestion.segments .map((segment) => (typeof segment === 'string' ? segment : segment.join(' '))) .join(''), } @@ -25,6 +25,6 @@ export function fuseIndex(stepDocuments: readonly StepDocument[]): Index { return (text) => { if (!text) return [] const results = fuse.search(text, { limit: 10 }) - return results.map((result) => stepDocuments[result.refIndex]) + return results.map((result) => suggestions[result.refIndex]) } } diff --git a/src/index/jsSearchIndex.ts b/src/index/jsSearchIndex.ts index e5cdc620..6f95a591 100644 --- a/src/index/jsSearchIndex.ts +++ b/src/index/jsSearchIndex.ts @@ -1,6 +1,6 @@ import { Search } from 'js-search' -import { StepDocument } from '../step-documents/types.js' +import { Suggestion } from '../suggestions/types.js' import { Index } from './types.js' type Doc = { @@ -8,11 +8,11 @@ type Doc = { text: string } -export function jsSearchIndex(stepDocuments: readonly StepDocument[]): Index { - const docs: Doc[] = stepDocuments.map((stepDocument, id) => { +export function jsSearchIndex(suggestions: readonly Suggestion[]): Index { + const docs: Doc[] = suggestions.map((suggestion, id) => { return { id, - text: stepDocument.segments + text: suggestion.segments .map((segment) => (typeof segment === 'string' ? segment : segment.join(' '))) .join(''), } @@ -25,6 +25,6 @@ export function jsSearchIndex(stepDocuments: readonly StepDocument[]): Index { return (text) => { if (!text) return [] const results = search.search(text) - return results.map((result: Doc) => stepDocuments[result.id]) + return results.map((result: Doc) => suggestions[result.id]) } } diff --git a/src/index/types.ts b/src/index/types.ts index f96dc1bb..63b0b470 100644 --- a/src/index/types.ts +++ b/src/index/types.ts @@ -2,8 +2,8 @@ * A search index function. * * @param text a text to search for - * @return results in the form of step documents + * @return results in the form of suggestions */ -import { StepDocument } from '../step-documents/types.js' +import { Suggestion } from '../suggestions/types.js' -export type Index = (text: string) => readonly StepDocument[] +export type Index = (text: string) => readonly Suggestion[] diff --git a/src/messages/MessagesBuilder.ts b/src/messages/MessagesBuilder.ts index 68e1b02d..523c0c53 100644 --- a/src/messages/MessagesBuilder.ts +++ b/src/messages/MessagesBuilder.ts @@ -7,11 +7,11 @@ import { import { Envelope, StepDefinitionPatternType } from '@cucumber/messages' import { extractStepTexts } from '../gherkin/extractStepTexts.js' -import { buildStepDocuments } from '../step-documents/buildStepDocuments.js' -import { StepDocument } from '../step-documents/types.js' +import { buildSuggestions } from '../suggestions/buildSuggestions.js' +import { Suggestion } from '../suggestions/types.js' export type MessagesBuilderResult = { - stepDocuments: readonly StepDocument[] + suggestions: readonly Suggestion[] expressions: readonly Expression[] } @@ -65,11 +65,7 @@ export class MessagesBuilder { build(): MessagesBuilderResult { return { - stepDocuments: buildStepDocuments( - this.parameterTypeRegistry, - this.stepTexts, - this.expressions - ), + suggestions: buildSuggestions(this.parameterTypeRegistry, this.stepTexts, this.expressions), expressions: this.expressions, } } diff --git a/src/service/getGherkinCompletionItems.ts b/src/service/getGherkinCompletionItems.ts index 3f539f08..14709ba8 100644 --- a/src/service/getGherkinCompletionItems.ts +++ b/src/service/getGherkinCompletionItems.ts @@ -28,13 +28,13 @@ export function getGherkinCompletionItems( }, }) if (text === undefined) return [] - const stepDocuments = index(text) - return stepDocuments.map((stepDocument) => ({ - label: stepDocument.suggestion, + const suggestions = index(text) + return suggestions.map((suggestion) => ({ + label: suggestion.label, insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Text, textEdit: { - newText: lspCompletionSnippet(stepDocument.segments), + newText: lspCompletionSnippet(suggestion.segments), range: { start: { line, diff --git a/src/service/snippet/lspCompletionSnippet.ts b/src/service/snippet/lspCompletionSnippet.ts index ffc1453a..2b6a82b0 100644 --- a/src/service/snippet/lspCompletionSnippet.ts +++ b/src/service/snippet/lspCompletionSnippet.ts @@ -3,11 +3,11 @@ * * @param expression the expression to generate the snippet from */ -import { StepSegments } from '../../step-documents/types.js' +import { SuggestionSegments } from '../../suggestions/types.js' -export function lspCompletionSnippet(stepSegments: StepSegments): string { +export function lspCompletionSnippet(segments: SuggestionSegments): string { let n = 1 - return stepSegments + return segments .map((segment) => (Array.isArray(segment) ? lspPlaceholder(n++, segment) : segment)) .join('') } diff --git a/src/step-documents/index.ts b/src/step-documents/index.ts deleted file mode 100644 index 2ef36595..00000000 --- a/src/step-documents/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './buildStepDocuments.js' -export * from './types.js' diff --git a/src/step-documents/types.ts b/src/step-documents/types.ts deleted file mode 100644 index 19088cad..00000000 --- a/src/step-documents/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * TODO: Rename to AutoCompleteItem - * A StepDocument is a data structure for auto-completion. - * - * TODO: Move this comment to the index - * A document that can be indexed. It's recommended to index the segments rather than the suggestion. - * When indexing the segments, the nested arrays (representing choices) may be given lower weight - * than the string segments (which represent the "sentence") - */ -export type StepDocument = { - /** - * The suggestion is what the user will see in the autocomplete. - */ - suggestion: string - - /** - * The segments are used to build the contents that will be inserted into the editor - * after selecting a suggestion. - * - * For LSP compatible editors, this can be formatted to an LSP snippet with the - * lspCompletionSnippet function. - */ - segments: StepSegments - - /** - * The Cucumber Expression or Regular Expression - */ - // expression: Expression -} - -export type StepSegments = readonly StepSegment[] -type Text = string -type Choices = readonly string[] -export type StepSegment = Text | Choices - -export type ParameterChoices = Record diff --git a/src/step-documents/buildStepDocumentFromCucumberExpression.ts b/src/suggestions/buildSuggestionFromCucumberExpression.ts similarity index 91% rename from src/step-documents/buildStepDocumentFromCucumberExpression.ts rename to src/suggestions/buildSuggestionFromCucumberExpression.ts index 55cb29cf..fa490cb6 100644 --- a/src/step-documents/buildStepDocumentFromCucumberExpression.ts +++ b/src/suggestions/buildSuggestionFromCucumberExpression.ts @@ -7,26 +7,26 @@ import { } from '@cucumber/cucumber-expressions' import { makeKey } from './helpers.js' -import { ParameterChoices, StepDocument, StepSegments } from './types.js' +import { ParameterChoices, Suggestion, SuggestionSegments } from './types.js' -export function buildStepDocumentFromCucumberExpression( +export function buildSuggestionFromCucumberExpression( expression: CucumberExpression, registry: ParameterTypeRegistry, parameterChoices: ParameterChoices -): StepDocument { +): Suggestion { const compiledSegments = compile(expression.ast, registry, parameterChoices) const segments = flatten(compiledSegments) return { - suggestion: expression.source, + label: expression.source, segments, } } type CompileResult = string | readonly CompileResult[] -function flatten(cr: CompileResult): StepSegments { +function flatten(cr: CompileResult): SuggestionSegments { if (typeof cr === 'string') return [cr] - return cr.reduce((prev, curr) => { + return cr.reduce((prev, curr) => { const last = prev[prev.length - 1] if (typeof curr === 'string') { if (typeof last === 'string') { diff --git a/src/step-documents/buildStepDocuments.ts b/src/suggestions/buildSuggestions.ts similarity index 60% rename from src/step-documents/buildStepDocuments.ts rename to src/suggestions/buildSuggestions.ts index fe1356b4..5ef7bf4d 100644 --- a/src/step-documents/buildStepDocuments.ts +++ b/src/suggestions/buildSuggestions.ts @@ -5,26 +5,26 @@ import { RegularExpression, } from '@cucumber/cucumber-expressions' -import { buildStepDocumentFromCucumberExpression } from './buildStepDocumentFromCucumberExpression.js' -import { buildStepDocumentsFromRegularExpression } from './buildStepDocumentsFromRegularExpression.js' +import { buildSuggestionFromCucumberExpression } from './buildSuggestionFromCucumberExpression.js' +import { buildSuggestionsFromRegularExpression } from './buildSuggestionsFromRegularExpression.js' import { makeKey } from './helpers.js' -import { StepDocument } from './types.js' +import { Suggestion } from './types.js' /** - * Builds an array of {@link StepDocument} from steps and step definitions. + * Builds an array of {@link Suggestion} from steps and step definitions. * - * @param parameterTypeRegistry + * @param registry * @param stepTexts * @param expressions * @param maxChoices */ -export function buildStepDocuments( - parameterTypeRegistry: ParameterTypeRegistry, +export function buildSuggestions( + registry: ParameterTypeRegistry, stepTexts: readonly string[], expressions: readonly Expression[], maxChoices = 10 -): readonly StepDocument[] { - let stepDocuments: StepDocument[] = [] +): readonly Suggestion[] { + let suggestions: Suggestion[] = [] const parameterChoiceSets: Record> = {} @@ -55,20 +55,15 @@ export function buildStepDocuments( for (const expression of expressions) { if (expression instanceof CucumberExpression) { - stepDocuments = stepDocuments.concat( - buildStepDocumentFromCucumberExpression(expression, parameterTypeRegistry, parameterChoices) + suggestions = suggestions.concat( + buildSuggestionFromCucumberExpression(expression, registry, parameterChoices) ) } if (expression instanceof RegularExpression) { - stepDocuments = stepDocuments.concat( - buildStepDocumentsFromRegularExpression( - expression, - parameterTypeRegistry, - stepTexts, - parameterChoices - ) + suggestions = suggestions.concat( + buildSuggestionsFromRegularExpression(expression, registry, stepTexts, parameterChoices) ) } } - return stepDocuments.sort((a, b) => a.suggestion.localeCompare(b.suggestion)) + return suggestions.sort((a, b) => a.label.localeCompare(b.label)) } diff --git a/src/step-documents/buildStepDocumentsFromRegularExpression.ts b/src/suggestions/buildSuggestionsFromRegularExpression.ts similarity index 78% rename from src/step-documents/buildStepDocumentsFromRegularExpression.ts rename to src/suggestions/buildSuggestionsFromRegularExpression.ts index 93dfebcb..f5f5499a 100644 --- a/src/step-documents/buildStepDocumentsFromRegularExpression.ts +++ b/src/suggestions/buildSuggestionsFromRegularExpression.ts @@ -1,20 +1,20 @@ import { ParameterTypeRegistry, RegularExpression } from '@cucumber/cucumber-expressions' -import { ParameterChoices, StepDocument, StepSegment } from './types' +import { ParameterChoices, Suggestion, SuggestionSegment } from './types' -export function buildStepDocumentsFromRegularExpression( +export function buildSuggestionsFromRegularExpression( expression: RegularExpression, registry: ParameterTypeRegistry, stepTexts: readonly string[], parameterChoices: ParameterChoices -): StepDocument[] { +): readonly Suggestion[] { const segmentJsons = new Set() for (const text of stepTexts) { const args = expression.match(text) if (args) { const parameterTypes = args.map((arg) => arg.getParameterType()) - const segments: StepSegment[] = [] + const segments: SuggestionSegment[] = [] let index = 0 for (let argIndex = 0; argIndex < args.length; argIndex++) { const arg = args[argIndex] @@ -37,7 +37,7 @@ export function buildStepDocumentsFromRegularExpression( } } return [...segmentJsons].sort().map((s, n) => ({ - segments: JSON.parse(s) as StepSegment[], - suggestion: n == 0 ? expression.source : `${expression.source} (${n + 1})`, + segments: JSON.parse(s) as SuggestionSegment[], + label: n == 0 ? expression.source : `${expression.source} (${n + 1})`, })) } diff --git a/src/step-documents/helpers.ts b/src/suggestions/helpers.ts similarity index 100% rename from src/step-documents/helpers.ts rename to src/suggestions/helpers.ts diff --git a/src/suggestions/index.ts b/src/suggestions/index.ts new file mode 100644 index 00000000..edd7c8a6 --- /dev/null +++ b/src/suggestions/index.ts @@ -0,0 +1,2 @@ +export * from './buildSuggestions.js' +export * from './types.js' diff --git a/src/suggestions/types.ts b/src/suggestions/types.ts new file mode 100644 index 00000000..6b54a6de --- /dev/null +++ b/src/suggestions/types.ts @@ -0,0 +1,22 @@ +export type Suggestion = { + /** + * The value that is presented to users in an autocomplete. + */ + label: string + + /** + * The segments are used to build the contents that will be inserted into the editor + * after selecting a suggestion. + * + * For LSP compatible editors, this can be formatted to an LSP snippet with the + * lspCompletionSnippet function. + */ + segments: SuggestionSegments +} + +export type SuggestionSegments = readonly SuggestionSegment[] +export type SuggestionSegment = Text | Choices +type Text = string +type Choices = readonly string[] + +export type ParameterChoices = Record diff --git a/test/index/Index.test.ts b/test/index/Index.test.ts index e28abbb9..67d46ed5 100644 --- a/test/index/Index.test.ts +++ b/test/index/Index.test.ts @@ -1,41 +1,40 @@ -import { ExpressionFactory, ParameterTypeRegistry } from '@cucumber/cucumber-expressions' import assert from 'assert' import * as txtgen from 'txtgen' import { bruteForceIndex, fuseIndex, Index, jsSearchIndex } from '../../src/index/index.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { Suggestion } from '../../src/suggestions/types.js' -type BuildIndex = (stepDocuments: readonly StepDocument[]) => Index +type BuildIndex = (suggestions: readonly Suggestion[]) => Index function verifyIndexContract(name: string, buildIndex: BuildIndex) { describe(name, () => { describe('basics', () => { - const doc1: StepDocument = { - suggestion: 'I have {int} cukes in my belly', + const s1: Suggestion = { + label: 'I have {int} cukes in my belly', segments: ['I have ', ['42', '98'], ' cukes in my belly'], } - const doc2: StepDocument = { - suggestion: 'I am a teapot', + const s2: Suggestion = { + label: 'I am a teapot', segments: ['I am a teapot'], } let index: Index beforeEach(() => { - index = buildIndex([doc1, doc2]) + index = buildIndex([s1, s2]) }) it('matches two words in the beginning of an expression', () => { const suggestions = index('have') - assert.deepStrictEqual(suggestions, [doc1]) + assert.deepStrictEqual(suggestions, [s1]) }) it('matches a word in an expression', () => { const suggestions = index('cukes') - assert.deepStrictEqual(suggestions, [doc1]) + assert.deepStrictEqual(suggestions, [s1]) }) it('matches a word in a choice', () => { const suggestions = index('98') - assert.deepStrictEqual(suggestions, [doc1]) + assert.deepStrictEqual(suggestions, [s1]) }) it('matches nothing', () => { @@ -47,22 +46,20 @@ function verifyIndexContract(name: string, buildIndex: BuildIndex) { if (!process.env.CI) { describe('performance / fuzz', () => { it('matches how quickly exactly?', () => { - const ef = new ExpressionFactory(new ParameterTypeRegistry()) for (let i = 0; i < 100; i++) { const length = 100 - const stepDocuments: StepDocument[] = Array(length) + const allSuggestions: Suggestion[] = Array(length) .fill(0) .map(() => { const sentence = txtgen.sentence() return { - suggestion: sentence, + label: sentence, segments: [sentence], - expression: ef.createExpression(sentence), } }) - const index = buildIndex(stepDocuments) + const index = buildIndex(allSuggestions) - const sentence = stepDocuments[Math.floor(length / 2)].segments[0] as string + const sentence = allSuggestions[Math.floor(length / 2)].segments[0] as string const words = sentence.split(' ') // Find a word longer than 5 letters (fall back to the middle word if there are none) const word = @@ -76,7 +73,7 @@ function verifyIndexContract(name: string, buildIndex: BuildIndex) { for (const suggestion of suggestions) { const s = (suggestion.segments[0] as string).toLowerCase() if (!s.includes(term)) { - // console.log(JSON.stringify(stepDocuments, null, 2)) + // console.log(JSON.stringify(suggestions, null, 2)) console.error(`WARNING: ${name} - "${s}" does not include "${term}"`) } } diff --git a/test/messages/MessagesBuilder.test.ts b/test/messages/MessagesBuilder.test.ts index 4c018621..9456ce07 100644 --- a/test/messages/MessagesBuilder.test.ts +++ b/test/messages/MessagesBuilder.test.ts @@ -5,7 +5,7 @@ import { pipeline as pipelineCb, Writable } from 'stream' import { promisify } from 'util' import { MessagesBuilderResult } from '../../src/messages/MessagesBuilder.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { Suggestion } from '../../src/suggestions/types.js' import { MessagesBuilderStream } from './MessagesBuilderStream.js' const pipeline = promisify(pipelineCb) @@ -63,46 +63,43 @@ describe('MessagesBuilder', () => { }, }) ) - const expectedStepDocuments: StepDocument[] = [ + const expectedSuggestions: Suggestion[] = [ { segments: ['I select the ', ['2nd'], ' snippet'], - suggestion: 'I select the {ordinal} snippet', + label: 'I select the {ordinal} snippet', }, { segments: [ 'I type ', ['"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}"', '"cukes"'], ], - suggestion: 'I type {string}', + label: 'I type {string}', }, { segments: ['the following Gherkin step texts exist:'], - suggestion: 'the following Gherkin step texts exist:', + label: 'the following Gherkin step texts exist:', }, { segments: ['the following Step Definitions exist:'], - suggestion: 'the following Step Definitions exist:', + label: 'the following Step Definitions exist:', }, { segments: [ 'the LSP snippet should be ', ['"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}"', '"cukes"'], ], - suggestion: 'the LSP snippet should be {string}', + label: 'the LSP snippet should be {string}', }, { segments: ['the suggestions should be empty'], - suggestion: 'the suggestions should be empty', + label: 'the suggestions should be empty', }, { segments: ['the suggestions should be:'], - suggestion: 'the suggestions should be:', + label: 'the suggestions should be:', }, ] - assert.deepStrictEqual( - result!.stepDocuments.map((d) => ({ segments: d.segments, suggestion: d.suggestion })), - expectedStepDocuments - ) + assert.deepStrictEqual(result!.suggestions, expectedSuggestions) const expectedExpressionSources = [ 'the following Gherkin step texts exist:', diff --git a/test/messages/messages.ndjson b/test/messages/messages.ndjson index ca64992d..42daa68a 100644 --- a/test/messages/messages.ndjson +++ b/test/messages/messages.ndjson @@ -1,6 +1,6 @@ {"meta":{"protocolVersion":"16.0.1","implementation":{"name":"cucumber-js","version":"7.3.1"},"cpu":{"name":"x64"},"os":{"name":"darwin","version":"20.5.0"},"runtime":{"name":"node.js","version":"16.3.0"}}} -{"source":{"data":"# Cucumber Suggest\n\nThis is a library that can be used to build Gherkin step auto-complete in editors.\nIt does not implement a UI component, but it can provide *suggestions* to an editor's auto-complete component.\n\nHere is an example of a [Monaco editor](https://microsoft.github.io/monaco-editor/) using this library:\n\n![Monaco](Monaco.gif)\n\n## Architecture\n\nThe suggest system consists of multiple components, each of which may run in a different process.\n\n```\n+--------------------+ +---------------+ +-------+\n| Cucumber/Regular | | | | |\n| Expressions | | Gherkin Steps | | |\n| (Step definitions) | | | | |\n+----------+---------+ +-------+-------+ | |\n | | | |\n | | | |\n +-------------+-----------+ | |\n | | |\n | | |\n +---------v----------+ | |\n | buildStepDocuments | | |\n | (Transform) | | ETL |\n +---------+----------+ | |\n | | |\n | | |\n | | |\n +-------v--------+ | |\n | Step Documents | | |\n +-------+--------+ | |\n | | |\n | | |\n | | |\n +-----v-----+ | |\n | Storage | | |\n +-----^-----+ +-------+\n |\n +--------+-------+\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Server | | Plugin |\n +---^----+ +----^----+\n | |\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Editor | |(Non-LSP)|\n +--------+ +---------+\n```\n\n### ETL process for Step Documents\n\nAt the top right of the diagram is an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load) process. Implementing\na full ETL process is currently beyond the scope of this library - it only implements the **transform** step. A full ETL\nprocess would do the following:\n\nFirst, **extract** [Cucumber Expressions](../../cucumber-expressions) and Regular Expressions from step definitions,\nand the text from Gherkin Steps. This can be done by parsing [Cucumber Messages](../../messages) from Cucumber dry-runs.\n\nSecond, **transform** the expressions and steps to [Step Documents](#step-documents) using the `buildStepDocuments` function.\n\nThird, **load** the *Step Documents* into a persistent storage. This can be a [search index](https://en.wikipedia.org/wiki/Search_engine_indexing),\nor some other form of persistent storage (such as a file in a file system or a database).\nSee the [Search Index](#search-index) section below for more details.\n\n### Editor suggest\n\nThis library does not implement any editor functionality, but it *does define* the *Step Document* data structure\non which editor auto-complete can be implemented. There are two ways to build support for an editor.\n\nWhat is common for both approaches is that they will query a search index.\n\nThe `StepDocument`s coming back from an index search can be converted to an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nusing the `lspCompletionSnippet` function.\n\nFor example, this `StepDocument`:\n\n`[\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]`\n\nbecomes the following LSP Completion Snippet:\n\n`I have ${1|42,54|} cukes in my ${2|basket,belly|}`\n\n### LSP\n\nWith the [LSP](https://microsoft.github.io/language-server-protocol/) approach, the storage is typically a \n[search index](https://en.wikipedia.org/wiki/Search_engine_indexing).\nWhen the user invokes the auto-complete command in the editor, the editor will send a \n[completion request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_completion) \nto the LSP server. The LSP server queries the search index, and uses the returned *Step Documents* to build the response to\nthe completion request.\n\n### Dedicated plugin\n\nWith the dedicated plugin approach, the storage is typically a file system or a database.\nWhen the editor plugin is loaded, it will fetch the *Step Documents* from the storage in raw form,\nand add them to an embedded (in-memory) search index.\n\nWhen the user invokes the auto-complete command in the editor, the editor plugin will query the in-memory search index\nand use the editor's native API to present the suggestions.\n\n## Examples\n\nThe examples below illustrate how the library works from the perspective of a user, with a full stack. The ETL process\nand the index all run in-memory.\n\n(Yep, this README.md file is executed by Cucumber.js)!\n\nThe suggestions in the examples use the\n[LSP Completion Snippet syntax](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nto represent search results.\n\n### Rule: Suggestions are based on both steps and step definitions\n\n#### Example: Two suggestions from Cucumber Expression\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | ------------------------------ |\n | I have 23 cukes in my belly |\n | I have 11 cukes on my table |\n | I have 11 cukes in my suitcase |\n | the weather forecast is rain |\n* And the following Step Definitions exist:\n | Cucumber Expression |\n | ---------------------------------- |\n | I have {int} cukes in/on my {word} |\n | the weather forecast is {word} |\n* When I type \"cukes\"\n* Then the suggestions should be:\n | Suggestion |\n | ------------------------------- |\n | I have {int} cukes in my {word} |\n | I have {int} cukes on my {word} |\n\n#### Example: One suggestion from Regular Expression\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | -------------------------------- |\n | I have 23 cukes in my \"belly\" |\n | I have 11 cukes in my \"suitcase\" |\n* And the following Step Definitions exist:\n | Regular Expression |\n | ----------------------------------------------- |\n | /I have (\\d\\d) cukes in my \"(belly\\|suitcase)\"/ |\n* When I type \"cukes\"\n* Then the suggestions should be:\n | Suggestion |\n | -------------------------- |\n | I have {} cukes in my \"{}\" |\n\nThe parameters are not named, because the regular expression doesn't have named capture groups.\n\n### Rule: Parameter choices are based on all steps\n\nThe available choices for a parameter type are built from *all* the choices\nencoutered for that parameter type, across steps.\n\n#### Example: {int} and {word} choices are build from three steps\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | -------------------------------- |\n | I have 23 cukes in my belly |\n | I have 11 cukes on my table |\n | there are 17 apples on the tree |\n* And the following Step Definitions exist:\n | Cucumber Expression |\n | ------------------------------------ |\n | I have {int} cukes in/on my {word} |\n | there are {int} apples on the {word} |\n* When I type \"cukes\"\n* And I select the 2nd snippet\n* Then the LSP snippet should be \"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}\"\n\nLSP-compatible editors such as\n[Monaco Editor](https://microsoft.github.io/monaco-editor/) or\n[Visual Studio Code](https://code.visualstudio.com/) can display these suggestions\nas `I have {int} cukes in my {word}` and `I have {int} cukes on my {word}`.\n\nWhen the user chooses a suggestion, the editor will focus the editor at the first parameter and\nlet the user choose between `11`, `17` or `23` (or type a custom value). When the user has made a choice,\nthe focus moves to the next parameter and suggests `belly`, `table` or `tree`.\n\n### Rule: Suggestions must have a matching step definition\n\nIt isn't enough to type something that matches an existing step -\nthe existing step must also have a matching step definition.\n\n#### Example: Nothing matches\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | --------------------------- |\n | I have 42 cukes in my belly |\n* And the following Step Definitions exist:\n | Step Definition Expression |\n | -------------------------- |\n | Something else |\n* When I type \"cukes\"\n* Then the suggestions should be empty\n\n### Step Documents\n\nA *Step Document* is a data structure with the following properties:\n\n* `suggestion` - what the user will see when the editor presents a suggestion\n* `segments` - what the editor will use to *insert* a suggestion, along with choices for parameters\n\nA *Step Document* can be represented as a JSON document. Here is an example:\n\n {\n \"suggestion\": \"I have {int} cukes in my belly\",\n \"segments\": [\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]\n }\n\nThe `segments` field is an array of `Text` (a string) or `Choices` (an array of strings).\nThe purpose of the `Choices` is to present the user with *possible* values for step parameters.\nThe segments above could be used to write the following steps texts:\n\n* `I have 42 cukes in my basket`\n* `I have 54 cukes in my basket`\n* `I have 42 cukes in my belly`\n* `I have 54 cukes in my belly`\n\nWhen a *Step Document* is added to a search index, it should use the `segments` field for indexing.\n\nThe `segments` field can also be used to build an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\n\n### Search Index\n\nEach `StepDocument` can be added to a *search index*, either during the ETL process, or inside a dedicated editor plugin. \nThe search index will return matching `StepDocument`s for a search term.\n\nThe index is a function with the following signature:\n\n`type Index = (text: string) => readonly StepDocument[]`\n\nThere are three experimental search index implementations in this library:\n\n* `fuseIndex` (based on [Fuse.js](https://fusejs.io/))\n* `jsSearchIndex` (based on [JS Search](http://bvaughn.github.io/js-search/))\n* `bruteForceIndex` (based on [String.prototype.includes()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes))\n\nThey are currently only in the test code, but one of them might be promoted to be part of the library at a later stage\nwhen we have tried them out on real data.\n\nSee the `Index.test.ts` contract test for more details about how the indexes behave.\n\n### Not in this library\n\nIt's beyond the scope of this library to implement an LSP server.\nAn LSP server could be built on this library though.\n\nIt is also beyond the scope of this library to provide any kind of UI component.\nFor LSP-capable editors this isn't even needed - it is built into the editor.\n\nFor non-LSP capable editors written in JavaScript (such as CodeMirror) it would be possible to\nbuild an auto-complete plugin that uses one of the `Index` implementations in this library.\nBuilding the `StepDocument`s could happen on a server somewhere, and could be transferred to\nthe browser over HTTP/JSON.\n","uri":"README.md","mediaType":"text/x.cucumber.gherkin+markdown"}} -{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","name":"# Cucumber Suggest","description":"```\n+--------------------+ +---------------+ +-------+\n| Cucumber/Regular | | | | |\n| Expressions | | Gherkin Steps | | |\n| (Step definitions) | | | | |\n+----------+---------+ +-------+-------+ | |\n | | | |\n | | | |\n +-------------+-----------+ | |\n | | |\n | | |\n +---------v----------+ | |\n | buildStepDocuments | | |\n | (Transform) | | ETL |\n +---------+----------+ | |\n | | |\n | | |\n | | |\n +-------v--------+ | |\n | Step Documents | | |\n +-------+--------+ | |\n | | |\n | | |\n | | |\n +-----v-----+ | |\n | Storage | | |\n +-----^-----+ +-------+\n |\n +--------+-------+\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Server | | Plugin |\n +---^----+ +----^----+\n | |\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Editor | |(Non-LSP)|\n +--------+ +---------+\n```\n\n### ETL process for Step Documents\n\nAt the top right of the diagram is an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load) process. Implementing\na full ETL process is currently beyond the scope of this library - it only implements the **transform** step. A full ETL\nprocess would do the following:\n\nFirst, **extract** [Cucumber Expressions](../../cucumber-expressions) and Regular Expressions from step definitions,\nand the text from Gherkin Steps. This can be done by parsing [Cucumber Messages](../../messages) from Cucumber dry-runs.\n\nSecond, **transform** the expressions and steps to [Step Documents](#step-documents) using the `buildStepDocuments` function.\n\nThird, **load** the *Step Documents* into a persistent storage. This can be a [search index](https://en.wikipedia.org/wiki/Search_engine_indexing),\nor some other form of persistent storage (such as a file in a file system or a database).\nSee the [Search Index](#search-index) section below for more details.\n\n### Editor suggest\n\nThis library does not implement any editor functionality, but it *does define* the *Step Document* data structure\non which editor auto-complete can be implemented. There are two ways to build support for an editor.\n\nWhat is common for both approaches is that they will query a search index.\n\nThe `StepDocument`s coming back from an index search can be converted to an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nusing the `lspCompletionSnippet` function.\n\nFor example, this `StepDocument`:\n\n`[\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]`\n\nbecomes the following LSP Completion Snippet:\n\n`I have ${1|42,54|} cukes in my ${2|basket,belly|}`\n\n### LSP\n\nWith the [LSP](https://microsoft.github.io/language-server-protocol/) approach, the storage is typically a \n[search index](https://en.wikipedia.org/wiki/Search_engine_indexing).\nWhen the user invokes the auto-complete command in the editor, the editor will send a \n[completion request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_completion) \nto the LSP server. The LSP server queries the search index, and uses the returned *Step Documents* to build the response to\nthe completion request.\n\n### Dedicated plugin\n\nWith the dedicated plugin approach, the storage is typically a file system or a database.\nWhen the editor plugin is loaded, it will fetch the *Step Documents* from the storage in raw form,\nand add them to an embedded (in-memory) search index.\n\nWhen the user invokes the auto-complete command in the editor, the editor plugin will query the in-memory search index\nand use the editor's native API to present the suggestions.\n\n## Examples\n\nThe examples below illustrate how the library works from the perspective of a user, with a full stack. The ETL process\nand the index all run in-memory.\n\n(Yep, this README.md file is executed by Cucumber.js)!\n\nThe suggestions in the examples use the\n[LSP Completion Snippet syntax](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nto represent search results.","children":[{"rule":{"id":"b8759cfc-df6d-47b5-8d4e-c173b385ae65","location":{"line":119,"column":5},"keyword":"Rule","name":"Suggestions are based on both steps and step definitions","description":"","children":[{"scenario":{"id":"5a67f15b-a967-477f-8d02-8f7d0a06a656","tags":[],"location":{"line":121,"column":6},"keyword":"Example","name":"Two suggestions from Cucumber Expression","description":"","steps":[{"id":"61b072e6-0a30-484b-a8cc-957a2f9d3e23","location":{"line":123,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":124,"column":3},"rows":[{"id":"2cb47494-61ad-4d42-ab1c-9ba842fc594e","location":{"line":124,"column":3},"cells":[{"location":{"line":124,"column":5},"value":"Gherkin Step"}]},{"id":"32ae561b-faeb-40f9-b3ed-1238e002d29d","location":{"line":126,"column":3},"cells":[{"location":{"line":126,"column":5},"value":"I have 23 cukes in my belly"}]},{"id":"0f21af80-cca9-4a46-be8c-6e57a8673882","location":{"line":127,"column":3},"cells":[{"location":{"line":127,"column":5},"value":"I have 11 cukes on my table"}]},{"id":"c6de230a-6427-42b0-a637-e05aa2d8b370","location":{"line":128,"column":3},"cells":[{"location":{"line":128,"column":5},"value":"I have 11 cukes in my suitcase"}]},{"id":"6b7c8dd5-6a93-4102-a56c-0ae91aa01611","location":{"line":129,"column":3},"cells":[{"location":{"line":129,"column":5},"value":"the weather forecast is rain"}]}]}},{"id":"2ca4f619-7c38-4d0f-8db2-7f5e44ff157a","location":{"line":130,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":131,"column":3},"rows":[{"id":"9bdec28e-adc8-4717-8e4b-1e844831d8fc","location":{"line":131,"column":3},"cells":[{"location":{"line":131,"column":5},"value":"Cucumber Expression"}]},{"id":"70241631-da5f-4fd0-950f-04ba43b11a3d","location":{"line":133,"column":3},"cells":[{"location":{"line":133,"column":5},"value":"I have {int} cukes in/on my {word}"}]},{"id":"cdfcac31-f2f4-43dd-8001-840afef89f77","location":{"line":134,"column":3},"cells":[{"location":{"line":134,"column":5},"value":"the weather forecast is {word}"}]}]}},{"id":"0e6423e3-6774-4c4d-a74d-b802655419ab","location":{"line":135,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"b314cb66-347d-44bd-a3ec-32a6e3bd6dfc","location":{"line":136,"column":3},"keyword":"Then ","text":"the suggestions should be:","dataTable":{"location":{"line":137,"column":3},"rows":[{"id":"db0a21d6-f842-4b79-8d01-3060c5400a3b","location":{"line":137,"column":3},"cells":[{"location":{"line":137,"column":5},"value":"Suggestion"}]},{"id":"8c5a9ee7-3169-4b14-9189-675b9913de15","location":{"line":139,"column":3},"cells":[{"location":{"line":139,"column":5},"value":"I have {int} cukes in my {word}"}]},{"id":"c2f33f2d-2578-4523-b7d4-cff577f12d6d","location":{"line":140,"column":3},"cells":[{"location":{"line":140,"column":5},"value":"I have {int} cukes on my {word}"}]}]}}],"examples":[]}},{"scenario":{"id":"f0c0e37a-a4af-4f7a-8f30-9a22dd18b196","tags":[],"location":{"line":142,"column":6},"keyword":"Example","name":"One suggestion from Regular Expression","description":"","steps":[{"id":"32ac7c49-4626-43a5-a62c-1e562a8a7288","location":{"line":144,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":145,"column":3},"rows":[{"id":"cae1e479-e809-47ed-9847-ce4f91c7bcac","location":{"line":145,"column":3},"cells":[{"location":{"line":145,"column":5},"value":"Gherkin Step"}]},{"id":"884017ac-ddbe-4a74-a62d-ea296addecc6","location":{"line":147,"column":3},"cells":[{"location":{"line":147,"column":5},"value":"I have 23 cukes in my \"belly\""}]},{"id":"22f72fe6-12cf-4f31-a2ce-b21fff4bc4d9","location":{"line":148,"column":3},"cells":[{"location":{"line":148,"column":5},"value":"I have 11 cukes in my \"suitcase\""}]}]}},{"id":"f5ffc2d2-3c98-4ac3-8f7e-a24ff154d857","location":{"line":149,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":150,"column":3},"rows":[{"id":"e3ba4f32-576e-497d-abed-72494b36bef5","location":{"line":150,"column":3},"cells":[{"location":{"line":150,"column":5},"value":"Regular Expression"}]},{"id":"7f31ca87-5cfa-416e-8a7b-b0aa1a7a2a13","location":{"line":152,"column":3},"cells":[{"location":{"line":152,"column":5},"value":"/I have (\\d\\d) cukes in my \"(belly|suitcase)\"/"}]}]}},{"id":"72d1607d-552b-4b37-a255-fe4b734ac27d","location":{"line":153,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"b01bde54-332c-4f0d-be93-32478381435d","location":{"line":154,"column":3},"keyword":"Then ","text":"the suggestions should be:","dataTable":{"location":{"line":155,"column":3},"rows":[{"id":"9104bc19-92a6-4a3a-9420-b4d949c81b6a","location":{"line":155,"column":3},"cells":[{"location":{"line":155,"column":5},"value":"Suggestion"}]},{"id":"11beeb05-0e99-4918-a9b6-31d9cc3dfca3","location":{"line":157,"column":3},"cells":[{"location":{"line":157,"column":5},"value":"I have {} cukes in my \"{}\""}]}]}}],"examples":[]}}],"tags":[]}},{"rule":{"id":"857e23da-9deb-4662-93b6-b0a3fbd96e1d","location":{"line":161,"column":5},"keyword":"Rule","name":"Parameter choices are based on all steps","description":"","children":[{"scenario":{"id":"9ddbc641-f622-4f0d-b4b9-c0d9c742bafb","tags":[],"location":{"line":166,"column":6},"keyword":"Example","name":"{int} and {word} choices are build from three steps","description":"","steps":[{"id":"b0e42663-9a3c-47ac-b0d4-f832b44f430b","location":{"line":168,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":169,"column":3},"rows":[{"id":"4916a097-95f1-45c7-aebc-858e1da56d50","location":{"line":169,"column":3},"cells":[{"location":{"line":169,"column":5},"value":"Gherkin Step"}]},{"id":"339b0a2c-9dbe-48a4-bdaa-dc9490eb033d","location":{"line":171,"column":3},"cells":[{"location":{"line":171,"column":5},"value":"I have 23 cukes in my belly"}]},{"id":"96fa4ab3-81db-45eb-b31b-8bb625c97efe","location":{"line":172,"column":3},"cells":[{"location":{"line":172,"column":5},"value":"I have 11 cukes on my table"}]},{"id":"8ee7ad05-e7c8-43ad-9fc3-9cd0d143a041","location":{"line":173,"column":3},"cells":[{"location":{"line":173,"column":5},"value":"there are 17 apples on the tree"}]}]}},{"id":"fd0ff74b-00d0-41a0-959f-6afd3dced0a2","location":{"line":174,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":175,"column":3},"rows":[{"id":"3c11a1ba-e8ed-4f64-b43c-8da5f5413546","location":{"line":175,"column":3},"cells":[{"location":{"line":175,"column":5},"value":"Cucumber Expression"}]},{"id":"a93d1f44-156e-4cc4-9296-176100b58c4f","location":{"line":177,"column":3},"cells":[{"location":{"line":177,"column":5},"value":"I have {int} cukes in/on my {word}"}]},{"id":"d16ba7f1-e584-4a51-ace3-590133067acc","location":{"line":178,"column":3},"cells":[{"location":{"line":178,"column":5},"value":"there are {int} apples on the {word}"}]}]}},{"id":"7127b950-0ae9-44c7-ae14-3f9edf6d6391","location":{"line":179,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"9db0cc93-416e-4baf-90f5-aa87638377c4","location":{"line":180,"column":3},"keyword":"And ","text":"I select the 2nd snippet"},{"id":"b99f85d1-2a1b-4986-b153-73fcb28e6fd4","location":{"line":181,"column":3},"keyword":"Then ","text":"the LSP snippet should be \"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}\""}],"examples":[]}}],"tags":[]}},{"rule":{"id":"32d38d18-d0f7-40b8-bca8-c5b5035a4c48","location":{"line":192,"column":5},"keyword":"Rule","name":"Suggestions must have a matching step definition","description":"","children":[{"scenario":{"id":"0f4e6378-2957-4954-b864-fd0cd7acb66b","tags":[],"location":{"line":197,"column":6},"keyword":"Example","name":"Nothing matches","description":"","steps":[{"id":"68c74927-3a93-4d5f-9095-19c8d8e60b4b","location":{"line":199,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":200,"column":3},"rows":[{"id":"c0eda36c-4b84-491e-92fd-9c6d8065aa00","location":{"line":200,"column":3},"cells":[{"location":{"line":200,"column":5},"value":"Gherkin Step"}]},{"id":"7f81eaec-4864-4f72-8613-4d2b7d6e851e","location":{"line":202,"column":3},"cells":[{"location":{"line":202,"column":5},"value":"I have 42 cukes in my belly"}]}]}},{"id":"da1663bd-9052-4b5f-b100-a0820121d928","location":{"line":203,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":204,"column":3},"rows":[{"id":"a6439f7c-d436-4a97-bf92-6bc7c3d83bc3","location":{"line":204,"column":3},"cells":[{"location":{"line":204,"column":5},"value":"Step Definition Expression"}]},{"id":"dc32b50b-5110-4e39-90d1-5bdca1961419","location":{"line":206,"column":3},"cells":[{"location":{"line":206,"column":5},"value":"Something else"}]}]}},{"id":"57adbdef-795b-4a9d-b31a-06275fc1cb8d","location":{"line":207,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"c5731fb0-3eb3-495e-9ac0-c522d5c56c56","location":{"line":208,"column":3},"keyword":"Then ","text":"the suggestions should be empty"}],"examples":[]}}],"tags":[]}}]},"comments":[],"uri":"README.md"}} +{"source":{"data":"# Cucumber Suggest\n\nThis is a library that can be used to build Gherkin step auto-complete in editors.\nIt does not implement a UI component, but it can provide *suggestions* to an editor's auto-complete component.\n\nHere is an example of a [Monaco editor](https://microsoft.github.io/monaco-editor/) using this library:\n\n![Monaco](Monaco.gif)\n\n## Architecture\n\nThe suggest system consists of multiple components, each of which may run in a different process.\n\n```\n+--------------------+ +---------------+ +-------+\n| Cucumber/Regular | | | | |\n| Expressions | | Gherkin Steps | | |\n| (Step definitions) | | | | |\n+----------+---------+ +-------+-------+ | |\n | | | |\n | | | |\n +-------------+-----------+ | |\n | | |\n | | |\n +---------v----------+ | |\n | buildSuggestions | | |\n | (Transform) | | ETL |\n +---------+----------+ | |\n | | |\n | | |\n | | |\n +-------v--------+ | |\n | Step Documents | | |\n +-------+--------+ | |\n | | |\n | | |\n | | |\n +-----v-----+ | |\n | Storage | | |\n +-----^-----+ +-------+\n |\n +--------+-------+\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Server | | Plugin |\n +---^----+ +----^----+\n | |\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Editor | |(Non-LSP)|\n +--------+ +---------+\n```\n\n### ETL process for Step Documents\n\nAt the top right of the diagram is an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load) process. Implementing\na full ETL process is currently beyond the scope of this library - it only implements the **transform** step. A full ETL\nprocess would do the following:\n\nFirst, **extract** [Cucumber Expressions](../../cucumber-expressions) and Regular Expressions from step definitions,\nand the text from Gherkin Steps. This can be done by parsing [Cucumber Messages](../../messages) from Cucumber dry-runs.\n\nSecond, **transform** the expressions and steps to [Step Documents](#step-documents) using the `buildSuggestions` function.\n\nThird, **load** the *Step Documents* into a persistent storage. This can be a [search index](https://en.wikipedia.org/wiki/Search_engine_indexing),\nor some other form of persistent storage (such as a file in a file system or a database).\nSee the [Search Index](#search-index) section below for more details.\n\n### Editor suggest\n\nThis library does not implement any editor functionality, but it *does define* the *Step Document* data structure\non which editor auto-complete can be implemented. There are two ways to build support for an editor.\n\nWhat is common for both approaches is that they will query a search index.\n\nThe `Suggestion`s coming back from an index search can be converted to an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nusing the `lspCompletionSnippet` function.\n\nFor example, this `Suggestion`:\n\n`[\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]`\n\nbecomes the following LSP Completion Snippet:\n\n`I have ${1|42,54|} cukes in my ${2|basket,belly|}`\n\n### LSP\n\nWith the [LSP](https://microsoft.github.io/language-server-protocol/) approach, the storage is typically a \n[search index](https://en.wikipedia.org/wiki/Search_engine_indexing).\nWhen the user invokes the auto-complete command in the editor, the editor will send a \n[completion request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_completion) \nto the LSP server. The LSP server queries the search index, and uses the returned *Step Documents* to build the response to\nthe completion request.\n\n### Dedicated plugin\n\nWith the dedicated plugin approach, the storage is typically a file system or a database.\nWhen the editor plugin is loaded, it will fetch the *Step Documents* from the storage in raw form,\nand add them to an embedded (in-memory) search index.\n\nWhen the user invokes the auto-complete command in the editor, the editor plugin will query the in-memory search index\nand use the editor's native API to present the suggestions.\n\n## Examples\n\nThe examples below illustrate how the library works from the perspective of a user, with a full stack. The ETL process\nand the index all run in-memory.\n\n(Yep, this README.md file is executed by Cucumber.js)!\n\nThe suggestions in the examples use the\n[LSP Completion Snippet syntax](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nto represent search results.\n\n### Rule: Suggestions are based on both steps and step definitions\n\n#### Example: Two suggestions from Cucumber Expression\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | ------------------------------ |\n | I have 23 cukes in my belly |\n | I have 11 cukes on my table |\n | I have 11 cukes in my suitcase |\n | the weather forecast is rain |\n* And the following Step Definitions exist:\n | Cucumber Expression |\n | ---------------------------------- |\n | I have {int} cukes in/on my {word} |\n | the weather forecast is {word} |\n* When I type \"cukes\"\n* Then the suggestions should be:\n | Suggestion |\n | ------------------------------- |\n | I have {int} cukes in my {word} |\n | I have {int} cukes on my {word} |\n\n#### Example: One suggestion from Regular Expression\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | -------------------------------- |\n | I have 23 cukes in my \"belly\" |\n | I have 11 cukes in my \"suitcase\" |\n* And the following Step Definitions exist:\n | Regular Expression |\n | ----------------------------------------------- |\n | /I have (\\d\\d) cukes in my \"(belly\\|suitcase)\"/ |\n* When I type \"cukes\"\n* Then the suggestions should be:\n | Suggestion |\n | -------------------------- |\n | I have {} cukes in my \"{}\" |\n\nThe parameters are not named, because the regular expression doesn't have named capture groups.\n\n### Rule: Parameter choices are based on all steps\n\nThe available choices for a parameter type are built from *all* the choices\nencoutered for that parameter type, across steps.\n\n#### Example: {int} and {word} choices are build from three steps\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | -------------------------------- |\n | I have 23 cukes in my belly |\n | I have 11 cukes on my table |\n | there are 17 apples on the tree |\n* And the following Step Definitions exist:\n | Cucumber Expression |\n | ------------------------------------ |\n | I have {int} cukes in/on my {word} |\n | there are {int} apples on the {word} |\n* When I type \"cukes\"\n* And I select the 2nd snippet\n* Then the LSP snippet should be \"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}\"\n\nLSP-compatible editors such as\n[Monaco Editor](https://microsoft.github.io/monaco-editor/) or\n[Visual Studio Code](https://code.visualstudio.com/) can display these suggestions\nas `I have {int} cukes in my {word}` and `I have {int} cukes on my {word}`.\n\nWhen the user chooses a suggestion, the editor will focus the editor at the first parameter and\nlet the user choose between `11`, `17` or `23` (or type a custom value). When the user has made a choice,\nthe focus moves to the next parameter and suggests `belly`, `table` or `tree`.\n\n### Rule: Suggestions must have a matching step definition\n\nIt isn't enough to type something that matches an existing step -\nthe existing step must also have a matching step definition.\n\n#### Example: Nothing matches\n\n* Given the following Gherkin step texts exist:\n | Gherkin Step |\n | --------------------------- |\n | I have 42 cukes in my belly |\n* And the following Step Definitions exist:\n | Step Definition Expression |\n | -------------------------- |\n | Something else |\n* When I type \"cukes\"\n* Then the suggestions should be empty\n\n### Step Documents\n\nA *Step Document* is a data structure with the following properties:\n\n* `suggestion` - what the user will see when the editor presents a suggestion\n* `segments` - what the editor will use to *insert* a suggestion, along with choices for parameters\n\nA *Step Document* can be represented as a JSON document. Here is an example:\n\n {\n \"suggestion\": \"I have {int} cukes in my belly\",\n \"segments\": [\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]\n }\n\nThe `segments` field is an array of `Text` (a string) or `Choices` (an array of strings).\nThe purpose of the `Choices` is to present the user with *possible* values for step parameters.\nThe segments above could be used to write the following steps texts:\n\n* `I have 42 cukes in my basket`\n* `I have 54 cukes in my basket`\n* `I have 42 cukes in my belly`\n* `I have 54 cukes in my belly`\n\nWhen a *Step Document* is added to a search index, it should use the `segments` field for indexing.\n\nThe `segments` field can also be used to build an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\n\n### Search Index\n\nEach `Suggestion` can be added to a *search index*, either during the ETL process, or inside a dedicated editor plugin. \nThe search index will return matching `Suggestion`s for a search term.\n\nThe index is a function with the following signature:\n\n`type Index = (text: string) => readonly Suggestion[]`\n\nThere are three experimental search index implementations in this library:\n\n* `fuseIndex` (based on [Fuse.js](https://fusejs.io/))\n* `jsSearchIndex` (based on [JS Search](http://bvaughn.github.io/js-search/))\n* `bruteForceIndex` (based on [String.prototype.includes()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes))\n\nThey are currently only in the test code, but one of them might be promoted to be part of the library at a later stage\nwhen we have tried them out on real data.\n\nSee the `Index.test.ts` contract test for more details about how the indexes behave.\n\n### Not in this library\n\nIt's beyond the scope of this library to implement an LSP server.\nAn LSP server could be built on this library though.\n\nIt is also beyond the scope of this library to provide any kind of UI component.\nFor LSP-capable editors this isn't even needed - it is built into the editor.\n\nFor non-LSP capable editors written in JavaScript (such as CodeMirror) it would be possible to\nbuild an auto-complete plugin that uses one of the `Index` implementations in this library.\nBuilding the `Suggestion`s could happen on a server somewhere, and could be transferred to\nthe browser over HTTP/JSON.\n","uri":"README.md","mediaType":"text/x.cucumber.gherkin+markdown"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","name":"# Cucumber Suggest","description":"```\n+--------------------+ +---------------+ +-------+\n| Cucumber/Regular | | | | |\n| Expressions | | Gherkin Steps | | |\n| (Step definitions) | | | | |\n+----------+---------+ +-------+-------+ | |\n | | | |\n | | | |\n +-------------+-----------+ | |\n | | |\n | | |\n +---------v----------+ | |\n | buildSuggestions | | |\n | (Transform) | | ETL |\n +---------+----------+ | |\n | | |\n | | |\n | | |\n +-------v--------+ | |\n | Step Documents | | |\n +-------+--------+ | |\n | | |\n | | |\n | | |\n +-----v-----+ | |\n | Storage | | |\n +-----^-----+ +-------+\n |\n +--------+-------+\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Server | | Plugin |\n +---^----+ +----^----+\n | |\n | |\n +---+----+ +----+----+\n | LSP | | Editor |\n | Editor | |(Non-LSP)|\n +--------+ +---------+\n```\n\n### ETL process for Step Documents\n\nAt the top right of the diagram is an [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load) process. Implementing\na full ETL process is currently beyond the scope of this library - it only implements the **transform** step. A full ETL\nprocess would do the following:\n\nFirst, **extract** [Cucumber Expressions](../../cucumber-expressions) and Regular Expressions from step definitions,\nand the text from Gherkin Steps. This can be done by parsing [Cucumber Messages](../../messages) from Cucumber dry-runs.\n\nSecond, **transform** the expressions and steps to [Step Documents](#step-documents) using the `buildSuggestions` function.\n\nThird, **load** the *Step Documents* into a persistent storage. This can be a [search index](https://en.wikipedia.org/wiki/Search_engine_indexing),\nor some other form of persistent storage (such as a file in a file system or a database).\nSee the [Search Index](#search-index) section below for more details.\n\n### Editor suggest\n\nThis library does not implement any editor functionality, but it *does define* the *Step Document* data structure\non which editor auto-complete can be implemented. There are two ways to build support for an editor.\n\nWhat is common for both approaches is that they will query a search index.\n\nThe `Suggestion`s coming back from an index search can be converted to an\n[LSP Completion Snippet](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nusing the `lspCompletionSnippet` function.\n\nFor example, this `Suggestion`:\n\n`[\"I have \", [\"42\", \"54\"], \" cukes in my \", [\"basket\", \"belly\"]]`\n\nbecomes the following LSP Completion Snippet:\n\n`I have ${1|42,54|} cukes in my ${2|basket,belly|}`\n\n### LSP\n\nWith the [LSP](https://microsoft.github.io/language-server-protocol/) approach, the storage is typically a \n[search index](https://en.wikipedia.org/wiki/Search_engine_indexing).\nWhen the user invokes the auto-complete command in the editor, the editor will send a \n[completion request](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_completion) \nto the LSP server. The LSP server queries the search index, and uses the returned *Step Documents* to build the response to\nthe completion request.\n\n### Dedicated plugin\n\nWith the dedicated plugin approach, the storage is typically a file system or a database.\nWhen the editor plugin is loaded, it will fetch the *Step Documents* from the storage in raw form,\nand add them to an embedded (in-memory) search index.\n\nWhen the user invokes the auto-complete command in the editor, the editor plugin will query the in-memory search index\nand use the editor's native API to present the suggestions.\n\n## Examples\n\nThe examples below illustrate how the library works from the perspective of a user, with a full stack. The ETL process\nand the index all run in-memory.\n\n(Yep, this README.md file is executed by Cucumber.js)!\n\nThe suggestions in the examples use the\n[LSP Completion Snippet syntax](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#snippet_syntax)\nto represent search results.","children":[{"rule":{"id":"b8759cfc-df6d-47b5-8d4e-c173b385ae65","location":{"line":119,"column":5},"keyword":"Rule","name":"Suggestions are based on both steps and step definitions","description":"","children":[{"scenario":{"id":"5a67f15b-a967-477f-8d02-8f7d0a06a656","tags":[],"location":{"line":121,"column":6},"keyword":"Example","name":"Two suggestions from Cucumber Expression","description":"","steps":[{"id":"61b072e6-0a30-484b-a8cc-957a2f9d3e23","location":{"line":123,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":124,"column":3},"rows":[{"id":"2cb47494-61ad-4d42-ab1c-9ba842fc594e","location":{"line":124,"column":3},"cells":[{"location":{"line":124,"column":5},"value":"Gherkin Step"}]},{"id":"32ae561b-faeb-40f9-b3ed-1238e002d29d","location":{"line":126,"column":3},"cells":[{"location":{"line":126,"column":5},"value":"I have 23 cukes in my belly"}]},{"id":"0f21af80-cca9-4a46-be8c-6e57a8673882","location":{"line":127,"column":3},"cells":[{"location":{"line":127,"column":5},"value":"I have 11 cukes on my table"}]},{"id":"c6de230a-6427-42b0-a637-e05aa2d8b370","location":{"line":128,"column":3},"cells":[{"location":{"line":128,"column":5},"value":"I have 11 cukes in my suitcase"}]},{"id":"6b7c8dd5-6a93-4102-a56c-0ae91aa01611","location":{"line":129,"column":3},"cells":[{"location":{"line":129,"column":5},"value":"the weather forecast is rain"}]}]}},{"id":"2ca4f619-7c38-4d0f-8db2-7f5e44ff157a","location":{"line":130,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":131,"column":3},"rows":[{"id":"9bdec28e-adc8-4717-8e4b-1e844831d8fc","location":{"line":131,"column":3},"cells":[{"location":{"line":131,"column":5},"value":"Cucumber Expression"}]},{"id":"70241631-da5f-4fd0-950f-04ba43b11a3d","location":{"line":133,"column":3},"cells":[{"location":{"line":133,"column":5},"value":"I have {int} cukes in/on my {word}"}]},{"id":"cdfcac31-f2f4-43dd-8001-840afef89f77","location":{"line":134,"column":3},"cells":[{"location":{"line":134,"column":5},"value":"the weather forecast is {word}"}]}]}},{"id":"0e6423e3-6774-4c4d-a74d-b802655419ab","location":{"line":135,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"b314cb66-347d-44bd-a3ec-32a6e3bd6dfc","location":{"line":136,"column":3},"keyword":"Then ","text":"the suggestions should be:","dataTable":{"location":{"line":137,"column":3},"rows":[{"id":"db0a21d6-f842-4b79-8d01-3060c5400a3b","location":{"line":137,"column":3},"cells":[{"location":{"line":137,"column":5},"value":"Suggestion"}]},{"id":"8c5a9ee7-3169-4b14-9189-675b9913de15","location":{"line":139,"column":3},"cells":[{"location":{"line":139,"column":5},"value":"I have {int} cukes in my {word}"}]},{"id":"c2f33f2d-2578-4523-b7d4-cff577f12d6d","location":{"line":140,"column":3},"cells":[{"location":{"line":140,"column":5},"value":"I have {int} cukes on my {word}"}]}]}}],"examples":[]}},{"scenario":{"id":"f0c0e37a-a4af-4f7a-8f30-9a22dd18b196","tags":[],"location":{"line":142,"column":6},"keyword":"Example","name":"One suggestion from Regular Expression","description":"","steps":[{"id":"32ac7c49-4626-43a5-a62c-1e562a8a7288","location":{"line":144,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":145,"column":3},"rows":[{"id":"cae1e479-e809-47ed-9847-ce4f91c7bcac","location":{"line":145,"column":3},"cells":[{"location":{"line":145,"column":5},"value":"Gherkin Step"}]},{"id":"884017ac-ddbe-4a74-a62d-ea296addecc6","location":{"line":147,"column":3},"cells":[{"location":{"line":147,"column":5},"value":"I have 23 cukes in my \"belly\""}]},{"id":"22f72fe6-12cf-4f31-a2ce-b21fff4bc4d9","location":{"line":148,"column":3},"cells":[{"location":{"line":148,"column":5},"value":"I have 11 cukes in my \"suitcase\""}]}]}},{"id":"f5ffc2d2-3c98-4ac3-8f7e-a24ff154d857","location":{"line":149,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":150,"column":3},"rows":[{"id":"e3ba4f32-576e-497d-abed-72494b36bef5","location":{"line":150,"column":3},"cells":[{"location":{"line":150,"column":5},"value":"Regular Expression"}]},{"id":"7f31ca87-5cfa-416e-8a7b-b0aa1a7a2a13","location":{"line":152,"column":3},"cells":[{"location":{"line":152,"column":5},"value":"/I have (\\d\\d) cukes in my \"(belly|suitcase)\"/"}]}]}},{"id":"72d1607d-552b-4b37-a255-fe4b734ac27d","location":{"line":153,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"b01bde54-332c-4f0d-be93-32478381435d","location":{"line":154,"column":3},"keyword":"Then ","text":"the suggestions should be:","dataTable":{"location":{"line":155,"column":3},"rows":[{"id":"9104bc19-92a6-4a3a-9420-b4d949c81b6a","location":{"line":155,"column":3},"cells":[{"location":{"line":155,"column":5},"value":"Suggestion"}]},{"id":"11beeb05-0e99-4918-a9b6-31d9cc3dfca3","location":{"line":157,"column":3},"cells":[{"location":{"line":157,"column":5},"value":"I have {} cukes in my \"{}\""}]}]}}],"examples":[]}}],"tags":[]}},{"rule":{"id":"857e23da-9deb-4662-93b6-b0a3fbd96e1d","location":{"line":161,"column":5},"keyword":"Rule","name":"Parameter choices are based on all steps","description":"","children":[{"scenario":{"id":"9ddbc641-f622-4f0d-b4b9-c0d9c742bafb","tags":[],"location":{"line":166,"column":6},"keyword":"Example","name":"{int} and {word} choices are build from three steps","description":"","steps":[{"id":"b0e42663-9a3c-47ac-b0d4-f832b44f430b","location":{"line":168,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":169,"column":3},"rows":[{"id":"4916a097-95f1-45c7-aebc-858e1da56d50","location":{"line":169,"column":3},"cells":[{"location":{"line":169,"column":5},"value":"Gherkin Step"}]},{"id":"339b0a2c-9dbe-48a4-bdaa-dc9490eb033d","location":{"line":171,"column":3},"cells":[{"location":{"line":171,"column":5},"value":"I have 23 cukes in my belly"}]},{"id":"96fa4ab3-81db-45eb-b31b-8bb625c97efe","location":{"line":172,"column":3},"cells":[{"location":{"line":172,"column":5},"value":"I have 11 cukes on my table"}]},{"id":"8ee7ad05-e7c8-43ad-9fc3-9cd0d143a041","location":{"line":173,"column":3},"cells":[{"location":{"line":173,"column":5},"value":"there are 17 apples on the tree"}]}]}},{"id":"fd0ff74b-00d0-41a0-959f-6afd3dced0a2","location":{"line":174,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":175,"column":3},"rows":[{"id":"3c11a1ba-e8ed-4f64-b43c-8da5f5413546","location":{"line":175,"column":3},"cells":[{"location":{"line":175,"column":5},"value":"Cucumber Expression"}]},{"id":"a93d1f44-156e-4cc4-9296-176100b58c4f","location":{"line":177,"column":3},"cells":[{"location":{"line":177,"column":5},"value":"I have {int} cukes in/on my {word}"}]},{"id":"d16ba7f1-e584-4a51-ace3-590133067acc","location":{"line":178,"column":3},"cells":[{"location":{"line":178,"column":5},"value":"there are {int} apples on the {word}"}]}]}},{"id":"7127b950-0ae9-44c7-ae14-3f9edf6d6391","location":{"line":179,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"9db0cc93-416e-4baf-90f5-aa87638377c4","location":{"line":180,"column":3},"keyword":"And ","text":"I select the 2nd snippet"},{"id":"b99f85d1-2a1b-4986-b153-73fcb28e6fd4","location":{"line":181,"column":3},"keyword":"Then ","text":"the LSP snippet should be \"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}\""}],"examples":[]}}],"tags":[]}},{"rule":{"id":"32d38d18-d0f7-40b8-bca8-c5b5035a4c48","location":{"line":192,"column":5},"keyword":"Rule","name":"Suggestions must have a matching step definition","description":"","children":[{"scenario":{"id":"0f4e6378-2957-4954-b864-fd0cd7acb66b","tags":[],"location":{"line":197,"column":6},"keyword":"Example","name":"Nothing matches","description":"","steps":[{"id":"68c74927-3a93-4d5f-9095-19c8d8e60b4b","location":{"line":199,"column":3},"keyword":"Given ","text":"the following Gherkin step texts exist:","dataTable":{"location":{"line":200,"column":3},"rows":[{"id":"c0eda36c-4b84-491e-92fd-9c6d8065aa00","location":{"line":200,"column":3},"cells":[{"location":{"line":200,"column":5},"value":"Gherkin Step"}]},{"id":"7f81eaec-4864-4f72-8613-4d2b7d6e851e","location":{"line":202,"column":3},"cells":[{"location":{"line":202,"column":5},"value":"I have 42 cukes in my belly"}]}]}},{"id":"da1663bd-9052-4b5f-b100-a0820121d928","location":{"line":203,"column":3},"keyword":"And ","text":"the following Step Definitions exist:","dataTable":{"location":{"line":204,"column":3},"rows":[{"id":"a6439f7c-d436-4a97-bf92-6bc7c3d83bc3","location":{"line":204,"column":3},"cells":[{"location":{"line":204,"column":5},"value":"Step Definition Expression"}]},{"id":"dc32b50b-5110-4e39-90d1-5bdca1961419","location":{"line":206,"column":3},"cells":[{"location":{"line":206,"column":5},"value":"Something else"}]}]}},{"id":"57adbdef-795b-4a9d-b31a-06275fc1cb8d","location":{"line":207,"column":3},"keyword":"When ","text":"I type \"cukes\""},{"id":"c5731fb0-3eb3-495e-9ac0-c522d5c56c56","location":{"line":208,"column":3},"keyword":"Then ","text":"the suggestions should be empty"}],"examples":[]}}],"tags":[]}}]},"comments":[],"uri":"README.md"}} {"pickle":{"id":"e3dc573e-e469-4229-9411-b55de9912276","uri":"README.md","astNodeIds":["5a67f15b-a967-477f-8d02-8f7d0a06a656"],"tags":[],"name":"Two suggestions from Cucumber Expression","language":"en","steps":[{"id":"76fc30b5-0ba4-49e5-b13f-a6e0042ae6c2","text":"the following Gherkin step texts exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Gherkin Step"}]},{"cells":[{"value":"I have 23 cukes in my belly"}]},{"cells":[{"value":"I have 11 cukes on my table"}]},{"cells":[{"value":"I have 11 cukes in my suitcase"}]},{"cells":[{"value":"the weather forecast is rain"}]}]}},"astNodeIds":["61b072e6-0a30-484b-a8cc-957a2f9d3e23"]},{"id":"584fe1d9-98dc-4fba-bedc-95ec3ad80fe9","text":"the following Step Definitions exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Cucumber Expression"}]},{"cells":[{"value":"I have {int} cukes in/on my {word}"}]},{"cells":[{"value":"the weather forecast is {word}"}]}]}},"astNodeIds":["2ca4f619-7c38-4d0f-8db2-7f5e44ff157a"]},{"id":"8052c87b-eba2-4fc4-ab25-b4c40ee06bb2","text":"I type \"cukes\"","astNodeIds":["0e6423e3-6774-4c4d-a74d-b802655419ab"]},{"id":"f4e9a058-6c66-419a-8c48-f3c2c798866e","text":"the suggestions should be:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Suggestion"}]},{"cells":[{"value":"I have {int} cukes in my {word}"}]},{"cells":[{"value":"I have {int} cukes on my {word}"}]}]}},"astNodeIds":["b314cb66-347d-44bd-a3ec-32a6e3bd6dfc"]}]}} {"pickle":{"id":"54fae48d-4496-4ccf-a93a-f7aa55c031a5","uri":"README.md","astNodeIds":["f0c0e37a-a4af-4f7a-8f30-9a22dd18b196"],"tags":[],"name":"One suggestion from Regular Expression","language":"en","steps":[{"id":"4e1963ac-fe43-454b-9e36-41300a5e93b9","text":"the following Gherkin step texts exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Gherkin Step"}]},{"cells":[{"value":"I have 23 cukes in my \"belly\""}]},{"cells":[{"value":"I have 11 cukes in my \"suitcase\""}]}]}},"astNodeIds":["32ac7c49-4626-43a5-a62c-1e562a8a7288"]},{"id":"15209eaf-0d7c-406f-9be1-b37d766680a5","text":"the following Step Definitions exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Regular Expression"}]},{"cells":[{"value":"/I have (\\d\\d) cukes in my \"(belly|suitcase)\"/"}]}]}},"astNodeIds":["f5ffc2d2-3c98-4ac3-8f7e-a24ff154d857"]},{"id":"0a24607e-175b-41c0-9d74-03d5b69675e5","text":"I type \"cukes\"","astNodeIds":["72d1607d-552b-4b37-a255-fe4b734ac27d"]},{"id":"0e58cf04-32a5-4c40-8796-0f0dc64a931a","text":"the suggestions should be:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Suggestion"}]},{"cells":[{"value":"I have {} cukes in my \"{}\""}]}]}},"astNodeIds":["b01bde54-332c-4f0d-be93-32478381435d"]}]}} {"pickle":{"id":"096db359-f2e5-4873-9315-3e180e3244ff","uri":"README.md","astNodeIds":["9ddbc641-f622-4f0d-b4b9-c0d9c742bafb"],"tags":[],"name":"{int} and {word} choices are build from three steps","language":"en","steps":[{"id":"2349f69f-5133-481e-89cf-dc17fe2e9789","text":"the following Gherkin step texts exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Gherkin Step"}]},{"cells":[{"value":"I have 23 cukes in my belly"}]},{"cells":[{"value":"I have 11 cukes on my table"}]},{"cells":[{"value":"there are 17 apples on the tree"}]}]}},"astNodeIds":["b0e42663-9a3c-47ac-b0d4-f832b44f430b"]},{"id":"8e5526da-fbc0-4246-bab4-d67c706d0883","text":"the following Step Definitions exist:","argument":{"dataTable":{"rows":[{"cells":[{"value":"Cucumber Expression"}]},{"cells":[{"value":"I have {int} cukes in/on my {word}"}]},{"cells":[{"value":"there are {int} apples on the {word}"}]}]}},"astNodeIds":["fd0ff74b-00d0-41a0-959f-6afd3dced0a2"]},{"id":"e6872fa8-fc8b-4cd3-a8c9-e92a5449e629","text":"I type \"cukes\"","astNodeIds":["7127b950-0ae9-44c7-ae14-3f9edf6d6391"]},{"id":"7ccda357-613c-47b7-ae4d-e2c58db393ac","text":"I select the 2nd snippet","astNodeIds":["9db0cc93-416e-4baf-90f5-aa87638377c4"]},{"id":"0cceb816-8508-4a79-8c51-8ba1b7b52356","text":"the LSP snippet should be \"I have ${1|11,17,23|} cukes on my ${2|belly,table,tree|}\"","astNodeIds":["b99f85d1-2a1b-4986-b153-73fcb28e6fd4"]}]}} diff --git a/test/service/getGherkinCompletionItems.test.ts b/test/service/getGherkinCompletionItems.test.ts index e01f4f22..45d16bdd 100644 --- a/test/service/getGherkinCompletionItems.test.ts +++ b/test/service/getGherkinCompletionItems.test.ts @@ -3,16 +3,16 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-lan import { bruteForceIndex } from '../../src/index/index.js' import { getGherkinCompletionItems } from '../../src/service/getGherkinCompletionItems.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { Suggestion } from '../../src/suggestions/types.js' describe('getGherkinCompletionItems', () => { it('completes with step text', () => { - const doc1: StepDocument = { - suggestion: 'I have {int} cukes in my belly', + const doc1: Suggestion = { + label: 'I have {int} cukes in my belly', segments: ['I have ', ['42', '98'], ' cukes in my belly'], } - const doc2: StepDocument = { - suggestion: 'I am a teapot', + const doc2: Suggestion = { + label: 'I am a teapot', segments: ['I am a teapot'], } diff --git a/test/service/snippet/lspCompletionSnippet.test.ts b/test/service/snippet/lspCompletionSnippet.test.ts index 5863b073..093d79c2 100644 --- a/test/service/snippet/lspCompletionSnippet.test.ts +++ b/test/service/snippet/lspCompletionSnippet.test.ts @@ -1,18 +1,18 @@ import assert from 'assert' import { lspCompletionSnippet } from '../../../src/service/snippet/lspCompletionSnippet.js' -import { StepSegments } from '../../../src/step-documents/types.js' +import { SuggestionSegments } from '../../../src/suggestions/types.js' describe('lspCompletionSnippet', () => { it('converts StepSegments to an LSP snippet', () => { - const stepSegments: StepSegments = [ + const segments: SuggestionSegments = [ 'I have ', ['42', '54'], ' cukes in my ', ['basket', 'belly', 'table'], ] assert.strictEqual( - lspCompletionSnippet(stepSegments), + lspCompletionSnippet(segments), 'I have ${1|42,54|} cukes in my ${2|basket,belly,table|}' ) }) diff --git a/test/step-documents/buildStepDocuments.test.ts b/test/suggestions/buildStepDocuments.test.ts similarity index 54% rename from test/step-documents/buildStepDocuments.test.ts rename to test/suggestions/buildStepDocuments.test.ts index fc514881..d6f85593 100644 --- a/test/step-documents/buildStepDocuments.test.ts +++ b/test/suggestions/buildStepDocuments.test.ts @@ -5,72 +5,38 @@ import { } from '@cucumber/cucumber-expressions' import assert from 'assert' -import { buildStepDocuments } from '../../src/step-documents/buildStepDocuments.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { buildSuggestions } from '../../src/suggestions/buildSuggestions.js' +import { Suggestion } from '../../src/suggestions/types.js' -describe('buildStepDocuments', () => { - xit('builds step documents from parameter step definition without steps', () => { - const parameterTypeRegistry = new ParameterTypeRegistry() - const ef = new ExpressionFactory(parameterTypeRegistry) - const expression = ef.createExpression('I have {int} cukes') - assertStepDocuments( - parameterTypeRegistry, - [], - [expression], - [ - { - suggestion: 'I have {int} cukes', - segments: ['I have ', [], ' cukes'], - }, - ] - ) - }) - - xit('builds step documents from alternation step definition without steps', () => { - const parameterTypeRegistry = new ParameterTypeRegistry() - const ef = new ExpressionFactory(parameterTypeRegistry) - const expression = ef.createExpression('I have two/three cukes') - assertStepDocuments( - parameterTypeRegistry, - [], - [expression], - [ - { - suggestion: 'I have two/three cukes', - segments: ['I have ', ['two', 'three'], ' cukes'], - }, - ] - ) - }) - - it('builds step documents with global choices', () => { +describe('buildSuggestions', () => { + it('builds suggestions with choices', () => { const parameterTypeRegistry = new ParameterTypeRegistry() const ef = new ExpressionFactory(parameterTypeRegistry) const e1 = ef.createExpression('The {word} song') const e2 = ef.createExpression('The {word} boat') - assertStepDocuments( + assertSuggestions( parameterTypeRegistry, ['The nice song', 'The big boat'], [e1, e2], [ { - suggestion: 'The {word} boat', + label: 'The {word} boat', segments: ['The ', ['big', 'nice'], ' boat'], }, { - suggestion: 'The {word} song', + label: 'The {word} song', segments: ['The ', ['big', 'nice'], ' song'], }, ] ) }) - it('builds step documents from CucumberExpression', () => { + it('builds suggestions from CucumberExpression', () => { const parameterTypeRegistry = new ParameterTypeRegistry() const ef = new ExpressionFactory(parameterTypeRegistry) const expression = ef.createExpression('I have {int} cukes in/on my {word}') - assertStepDocuments( + assertSuggestions( parameterTypeRegistry, [ 'I have 42 cukes in my belly', @@ -80,7 +46,7 @@ describe('buildStepDocuments', () => { [expression], [ { - suggestion: 'I have {int} cukes in/on my {word}', + label: 'I have {int} cukes in/on my {word}', segments: [ 'I have ', ['42', '54'], @@ -94,28 +60,28 @@ describe('buildStepDocuments', () => { ) }) - it('builds step documents from RegularExpression', () => { + it('builds suggestions from RegularExpression', () => { const parameterTypeRegistry = new ParameterTypeRegistry() const ef = new ExpressionFactory(parameterTypeRegistry) const expression = ef.createExpression(/I have (\d\d) cukes in my "(belly|suitcase)"/) - assertStepDocuments( + assertSuggestions( parameterTypeRegistry, ['I have 42 cukes in my "belly"', 'I have 54 cukes in my "suitcase"'], [expression], [ { - suggestion: 'I have (\\d\\d) cukes in my "(belly|suitcase)"', + label: 'I have (\\d\\d) cukes in my "(belly|suitcase)"', segments: ['I have ', ['42', '54'], ' cukes in my "', ['belly', 'suitcase'], '"'], }, ] ) }) - it('builds step documents with a max number of choices', () => { + it('builds suggestions with a max number of choices', () => { const parameterTypeRegistry = new ParameterTypeRegistry() const ef = new ExpressionFactory(parameterTypeRegistry) const expression = ef.createExpression('I have {int} cukes in/on my {word}') - assertStepDocuments( + assertSuggestions( parameterTypeRegistry, [ 'I have 42 cukes in my belly', @@ -126,7 +92,7 @@ describe('buildStepDocuments', () => { [expression], [ { - suggestion: 'I have {int} cukes in/on my {word}', + label: 'I have {int} cukes in/on my {word}', segments: ['I have ', ['42', '54'], ' cukes ', ['in', 'on'], ' my ', ['basket', 'belly']], }, ], @@ -135,18 +101,13 @@ describe('buildStepDocuments', () => { }) }) -function assertStepDocuments( +function assertSuggestions( parameterTypeRegistry: ParameterTypeRegistry, stepTexts: readonly string[], expressions: readonly Expression[], - expectedStepDocuments: StepDocument[], + expectedSuggestions: Suggestion[], maxChoices = 10 ) { - const stepDocuments = buildStepDocuments( - parameterTypeRegistry, - stepTexts, - expressions, - maxChoices - ) - assert.deepStrictEqual(stepDocuments, expectedStepDocuments) + const suggestions = buildSuggestions(parameterTypeRegistry, stepTexts, expressions, maxChoices) + assert.deepStrictEqual(suggestions, expectedSuggestions) } diff --git a/test/step-documents/buildStepDocumentsFromRegularExpression.test.ts b/test/suggestions/buildStepDocumentsFromRegularExpression.test.ts similarity index 55% rename from test/step-documents/buildStepDocumentsFromRegularExpression.test.ts rename to test/suggestions/buildStepDocumentsFromRegularExpression.test.ts index e126faa1..8f7a91c8 100644 --- a/test/step-documents/buildStepDocumentsFromRegularExpression.test.ts +++ b/test/suggestions/buildStepDocumentsFromRegularExpression.test.ts @@ -1,10 +1,10 @@ import { ParameterTypeRegistry, RegularExpression } from '@cucumber/cucumber-expressions' import assert from 'assert' -import { buildStepDocumentsFromRegularExpression } from '../../src/step-documents/buildStepDocumentsFromRegularExpression.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { buildSuggestionsFromRegularExpression } from '../../src/suggestions/buildSuggestionsFromRegularExpression.js' +import { Suggestion } from '../../src/suggestions/types.js' -describe('buildStepDocumentsFromRegularExpression', () => { +describe('buildSuggestionsFromRegularExpression', () => { let registry: ParameterTypeRegistry beforeEach(() => { registry = new ParameterTypeRegistry() @@ -12,11 +12,11 @@ describe('buildStepDocumentsFromRegularExpression', () => { it('builds an item from a plain expression', () => { const expression = new RegularExpression(/I have 4 cukes/, registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have 4 cukes'], - suggestion: 'I have 4 cukes', + label: 'I have 4 cukes', } - const actual = buildStepDocumentsFromRegularExpression( + const actual = buildSuggestionsFromRegularExpression( expression, registry, ['I have 4 cukes'], @@ -27,16 +27,13 @@ describe('buildStepDocumentsFromRegularExpression', () => { it('builds an item from an expression with a group', () => { const expression = new RegularExpression(/I have (\d+) cukes/, registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have ', ['12'], ' cukes'], - suggestion: 'I have (\\d+) cukes', + label: 'I have (\\d+) cukes', } - const actual = buildStepDocumentsFromRegularExpression( - expression, - registry, - ['I have 4 cukes'], - { '-?\\d+|\\d+': ['12'] } - ) + const actual = buildSuggestionsFromRegularExpression(expression, registry, ['I have 4 cukes'], { + '-?\\d+|\\d+': ['12'], + }) assert.deepStrictEqual(actual, [expected]) }) }) diff --git a/test/step-documents/buildStepDocumentFromCucumberExpression.test.ts b/test/suggestions/buildSuggestionFromCucumberExpression.test.ts similarity index 59% rename from test/step-documents/buildStepDocumentFromCucumberExpression.test.ts rename to test/suggestions/buildSuggestionFromCucumberExpression.test.ts index 87d4612b..c586f2ae 100644 --- a/test/step-documents/buildStepDocumentFromCucumberExpression.test.ts +++ b/test/suggestions/buildSuggestionFromCucumberExpression.test.ts @@ -1,10 +1,10 @@ import { CucumberExpression, ParameterTypeRegistry } from '@cucumber/cucumber-expressions' import assert from 'assert' -import { buildStepDocumentFromCucumberExpression } from '../../src/step-documents/buildStepDocumentFromCucumberExpression.js' -import { StepDocument } from '../../src/step-documents/types.js' +import { buildSuggestionFromCucumberExpression } from '../../src/suggestions/buildSuggestionFromCucumberExpression.js' +import { Suggestion } from '../../src/suggestions/types.js' -describe('buildStepDocumentFromCucumberExpression', () => { +describe('buildSuggestionFromCucumberExpression', () => { let registry: ParameterTypeRegistry beforeEach(() => { registry = new ParameterTypeRegistry() @@ -12,41 +12,41 @@ describe('buildStepDocumentFromCucumberExpression', () => { it('builds an item from plain expression', () => { const expression = new CucumberExpression('I have 4 cukes', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have 4 cukes'], - suggestion: 'I have 4 cukes', + label: 'I have 4 cukes', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, {}) + const actual = buildSuggestionFromCucumberExpression(expression, registry, {}) assert.deepStrictEqual(actual, expected) }) it('builds an item from alternation expression', () => { const expression = new CucumberExpression('I have 4/5 cukes', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have ', ['4', '5'], ' cukes'], - suggestion: 'I have 4/5 cukes', + label: 'I have 4/5 cukes', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, {}) + const actual = buildSuggestionFromCucumberExpression(expression, registry, {}) assert.deepStrictEqual(actual, expected) }) it('builds an item from optional expression', () => { const expression = new CucumberExpression('I have 1 cuke(s)', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have 1 cuke', ['s', '']], - suggestion: 'I have 1 cuke(s)', + label: 'I have 1 cuke(s)', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, {}) + const actual = buildSuggestionFromCucumberExpression(expression, registry, {}) assert.deepStrictEqual(actual, expected) }) it('builds an item from parameter expression with explicit options', () => { const expression = new CucumberExpression('I have {int} cukes', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have ', ['12', '17'], ' cukes'], - suggestion: 'I have {int} cukes', + label: 'I have {int} cukes', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, { + const actual = buildSuggestionFromCucumberExpression(expression, registry, { int: ['12', '17'], }) assert.deepStrictEqual(actual, expected) @@ -54,31 +54,31 @@ describe('buildStepDocumentFromCucumberExpression', () => { it('builds an item from int parameter expression without explicit options', () => { const expression = new CucumberExpression('I have {int} cukes', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have ', ['0'], ' cukes'], - suggestion: 'I have {int} cukes', + label: 'I have {int} cukes', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, {}) + const actual = buildSuggestionFromCucumberExpression(expression, registry, {}) assert.deepStrictEqual(actual, expected) }) it('builds an item from only alternation expression', () => { const expression = new CucumberExpression('me/you', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: [['me', 'you']], - suggestion: 'me/you', + label: 'me/you', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, {}) + const actual = buildSuggestionFromCucumberExpression(expression, registry, {}) assert.deepStrictEqual(actual, expected) }) it('builds an item from complex expression', () => { const expression = new CucumberExpression('I have {int} cuke(s) in my bag/belly', registry) - const expected: StepDocument = { + const expected: Suggestion = { segments: ['I have ', ['12'], ' cuke', ['s', ''], ' in my ', ['bag', 'belly']], - suggestion: 'I have {int} cuke(s) in my bag/belly', + label: 'I have {int} cuke(s) in my bag/belly', } - const actual = buildStepDocumentFromCucumberExpression(expression, registry, { + const actual = buildSuggestionFromCucumberExpression(expression, registry, { int: ['12'], }) assert.deepStrictEqual(actual, expected)