From 12c9e76d0391b1e0b286680ccc404b0c3905f683 Mon Sep 17 00:00:00 2001 From: angelozerr Date: Fri, 9 Apr 2021 17:19:06 +0200 Subject: [PATCH] Completion for available topic in kafka file Fixes #150 Signed-off-by: azerr --- CHANGELOG.md | 1 + docs/Consuming.md | 4 + docs/Producing.md | 6 +- .../kafka-file-consumer-topic-completion.png | Bin 0 -> 16170 bytes .../kafka-file-producer-topic-completion.png | Bin 0 -> 18407 bytes src/explorer/kafkaExplorer.ts | 22 ++- src/explorer/models/cluster.ts | 16 +- src/explorer/models/kafka.ts | 12 +- src/extension.ts | 2 +- src/kafka-file/kafkaFileClient.ts | 26 ++- .../kafkaFileLanguageService.ts | 22 ++- .../services/codeLensProvider.ts | 3 + .../languageservice/services/completion.ts | 174 +++++++++++++----- .../languageservice/codeLens.test.ts | 42 ++--- .../languageservice/completionTopic.test.ts | 64 +++++++ .../kafka-file/languageservice/kafkaAssert.ts | 28 +-- 16 files changed, 313 insertions(+), 109 deletions(-) create mode 100644 docs/assets/kafka-file-consumer-topic-completion.png create mode 100644 docs/assets/kafka-file-producer-topic-completion.png create mode 100644 src/test/suite/kafka-file/languageservice/completionTopic.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d398dab..f2ba6e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to `Tools for Apache Kafka®` are documented in this file. - Declare key/value formats for PRODUCER in kafka file. See [#113](https://github.com/jlandersen/vscode-kafka/issues/113). - Completion support for property names and values of CONSUMER and PRODUCER blocks. See [#146](https://github.com/jlandersen/vscode-kafka/issues/146). - Completion support for fakerJS PRODUCER key and value. See [#152](https://github.com/jlandersen/vscode-kafka/issues/152). +- Completion support for available topics for CONSUMER and PRODUCER blocks. See [#150](https://github.com/jlandersen/vscode-kafka/issues/150). ### Changed - Renamed extension as `Tools for Apache Kafka®` diff --git a/docs/Consuming.md b/docs/Consuming.md index 4260b89..bcfa595 100644 --- a/docs/Consuming.md +++ b/docs/Consuming.md @@ -85,6 +85,10 @@ Completion is available for ![Property value completion](assets/kafka-file-consumer-property-value-completion.png) + * topic: + +![Topic completion](assets/kafka-file-consumer-topic-completion.png) + ### Start Consumer command ![Start Consumer from command palette](assets/start-consumer-from-command.png) diff --git a/docs/Producing.md b/docs/Producing.md index 423c061..3ae9e51 100644 --- a/docs/Producing.md +++ b/docs/Producing.md @@ -59,10 +59,14 @@ Completion is available for ![Property value completion](assets/kafka-file-producer-property-value-completion.png) - * randomized content (see following section) + * randomized content (see following section): ![FakerJS completion](assets/kafka-file-producer-fakerjs-completion.png) + * topic: + +![Topic completion](assets/kafka-file-producer-topic-completion.png) + ## Randomized content Record content can be randomized by injecting mustache-like placeholders of [faker.js properties](https://github.com/Marak/faker.js#api-methods), like ``{{name.lastName}}`` or ``{{random.number}}``. Some randomized properties can be localized via the `kafka.producers.fakerjs.locale` setting. diff --git a/docs/assets/kafka-file-consumer-topic-completion.png b/docs/assets/kafka-file-consumer-topic-completion.png new file mode 100644 index 0000000000000000000000000000000000000000..2146f54986be39e1230a06e0f9ee567802872e75 GIT binary patch literal 16170 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 uQdJyBy>#JHHboKA%)N2OU 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 { +export class KafkaExplorer implements KafkaModelProvider, vscode.Disposable, vscode.TreeDataProvider { private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); @@ -63,10 +63,7 @@ export class KafkaExplorer implements vscode.Disposable, vscode.TreeDataProvider async getChildren(element?: NodeBase): Promise { if (!element) { - if (!this.root) { - this.root = new KafkaModel(this.clusterSettings, this.clientAccessor); - } - element = this.root; + element = this.getDataModel(); } return element.getChildren(); } @@ -180,4 +177,17 @@ export class KafkaExplorer implements vscode.Disposable, vscode.TreeDataProvider } } + + /** + * Returns the kafka data model. + * + * @returns the kafka data model. + */ + public getDataModel(): KafkaModel { + if (!this.root) { + this.root = new KafkaModel(this.clusterSettings, this.clientAccessor); + } + return this.root; + } + } diff --git a/src/explorer/models/cluster.ts b/src/explorer/models/cluster.ts index 701f09c..73b5e11 100644 --- a/src/explorer/models/cluster.ts +++ b/src/explorer/models/cluster.ts @@ -61,10 +61,16 @@ export class ClusterItem extends NodeBase implements Disposable { } async findTopictemByName(topicName: string): Promise { - const topics = (await this.getTopicGroupItem()).getChildren(); - return topics - .then(t => - t.find(child => (child).topic.id === topicName)); + const topics = await this.getTopics(); + return topics.find(child => (child).topic.id === topicName); + } + + /** + * Returns the topics of the cluster. + * @returns the topics of the cluster. + */ + async getTopics() { + return (await this.getTopicGroupItem()).getChildren(); } private async getTopicGroupItem(): Promise { @@ -72,3 +78,5 @@ export class ClusterItem extends NodeBase implements Disposable { } } + + diff --git a/src/explorer/models/kafka.ts b/src/explorer/models/kafka.ts index 4911425..c845103 100644 --- a/src/explorer/models/kafka.ts +++ b/src/explorer/models/kafka.ts @@ -4,6 +4,10 @@ import { ClusterSettings } from "../../settings"; import { ClusterItem } from "./cluster"; import { NodeBase } from "./nodeBase"; +export interface KafkaModelProvider { + getDataModel(): KafkaModel; +} + export class KafkaModel extends NodeBase implements Disposable { public contextValue = ""; @@ -33,10 +37,8 @@ export class KafkaModel extends NodeBase implements Disposable { ); } - async findClusterItemById(clusterId: string): Promise { - return this.getChildren() - .then(clusters => - clusters.find(child => (child).cluster.id === clusterId) - ); + async findClusterItemById(clusterId: string): Promise { + const clusters = await this.getChildren(); + return clusters.find(child => (child).cluster.id === clusterId); } } diff --git a/src/extension.ts b/src/extension.ts index dba0328..f775d19 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -138,7 +138,7 @@ export function activate(context: vscode.ExtensionContext): KafkaExtensionPartic // .kafka file related context.subscriptions.push( - startLanguageClient(clusterSettings, producerCollection, consumerCollection, context) + startLanguageClient(clusterSettings, producerCollection, consumerCollection, explorer, context) ); context.subscriptions.push( diff --git a/src/kafka-file/kafkaFileClient.ts b/src/kafka-file/kafkaFileClient.ts index a7404d6..c2f5246 100644 --- a/src/kafka-file/kafkaFileClient.ts +++ b/src/kafka-file/kafkaFileClient.ts @@ -5,13 +5,16 @@ import { ClusterSettings } from "../settings/clusters"; import { getLanguageModelCache, LanguageModelCache } from './languageModelCache'; import { KafkaFileDocument } from "./languageservice/parser/kafkaFileParser"; -import { ConsumerLaunchStateProvider, getLanguageService, LanguageService, ProducerLaunchStateProvider, SelectedClusterProvider } from "./languageservice/kafkaFileLanguageService"; +import { ConsumerLaunchStateProvider, getLanguageService, LanguageService, ProducerLaunchStateProvider, SelectedClusterProvider, TopicDetail, TopicProvider } from "./languageservice/kafkaFileLanguageService"; import { runSafeAsync } from "./utils/runner"; +import { TopicItem } from "../explorer"; +import { KafkaModelProvider } from "../explorer/models/kafka"; export function startLanguageClient( clusterSettings: ClusterSettings, producerCollection: ProducerCollection, consumerCollection: ConsumerCollection, + modelProvider: KafkaModelProvider, context: vscode.ExtensionContext ): vscode.Disposable { @@ -20,7 +23,7 @@ export function startLanguageClient( const kafkaFileDocuments = getLanguageModelCache(10, 60, document => languageService.parseKafkaFileDocument(document)); // Create the Kafka file language service. - const languageService = createLanguageService(clusterSettings, producerCollection, consumerCollection); + const languageService = createLanguageService(clusterSettings, producerCollection, consumerCollection, modelProvider); // Open / Close document context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(e => { @@ -74,7 +77,7 @@ export function startLanguageClient( }; } -function createLanguageService(clusterSettings: ClusterSettings, producerCollection: ProducerCollection, consumerCollection: ConsumerCollection): LanguageService { +function createLanguageService(clusterSettings: ClusterSettings, producerCollection: ProducerCollection, consumerCollection: ConsumerCollection, modelProvider: KafkaModelProvider): LanguageService { const producerLaunchStateProvider = { getProducerLaunchState(uri: vscode.Uri): ProducerLaunchState { const producer = producerCollection.get(uri); @@ -90,7 +93,6 @@ function createLanguageService(clusterSettings: ClusterSettings, producerCollect } as ConsumerLaunchStateProvider; const selectedClusterProvider = { - getSelectedCluster() { const selected = clusterSettings.selected; return { @@ -98,10 +100,22 @@ function createLanguageService(clusterSettings: ClusterSettings, producerCollect clusterName: selected?.name, }; } - } as SelectedClusterProvider; - return getLanguageService(producerLaunchStateProvider, consumerLaunchStateProvider, selectedClusterProvider); + const topicProvider = { + async getTopics(clusterId: string): Promise { + // Retrieve the proper cluster item from the explorer + const model = modelProvider.getDataModel(); + const cluster = await model.findClusterItemById(clusterId); + if (!cluster) { + return []; + } + // Returns topics from the cluster + return (await cluster.getTopics()).map(child => (child).topic); + } + } as TopicProvider; + + return getLanguageService(producerLaunchStateProvider, consumerLaunchStateProvider, selectedClusterProvider, topicProvider); } class AbstractKafkaFileFeature { diff --git a/src/kafka-file/languageservice/kafkaFileLanguageService.ts b/src/kafka-file/languageservice/kafkaFileLanguageService.ts index 384da38..654f448 100644 --- a/src/kafka-file/languageservice/kafkaFileLanguageService.ts +++ b/src/kafka-file/languageservice/kafkaFileLanguageService.ts @@ -26,6 +26,19 @@ export interface SelectedClusterProvider { getSelectedCluster(): { clusterId?: string, clusterName?: string }; } +export interface TopicDetail { + id: string; + partitionCount: number; + replicationFactor: number; +} + +/** + * Provider API which gets topics from given cluster id. + */ +export interface TopicProvider { + getTopics(clusterid: string): Promise; +} + /** * Kafka language service API. * @@ -52,12 +65,12 @@ export interface LanguageService { /** * Returns the completion result for the given text document and parsed AST at given position. - * + * * @param document the text document. * @param kafkaFileDocument the parsed AST. * @param position the position where the completion was triggered. */ - doComplete(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): CompletionList | undefined + doComplete(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): Promise } /** @@ -66,11 +79,12 @@ export interface LanguageService { * @param producerLaunchStateProvider the provider which gets the state for a given producer. * @param consumerLaunchStateProvider the provider which gets the state for a given consumer. * @param selectedClusterProvider the provider which gets the selected cluster id and name. + * @param topicProvider the provider which returns topics from a given cluster id. */ -export function getLanguageService(producerLaunchStateProvider: ProducerLaunchStateProvider, consumerLaunchStateProvider: ConsumerLaunchStateProvider, selectedClusterProvider: SelectedClusterProvider): LanguageService { +export function getLanguageService(producerLaunchStateProvider: ProducerLaunchStateProvider, consumerLaunchStateProvider: ConsumerLaunchStateProvider, selectedClusterProvider: SelectedClusterProvider, topicProvider: TopicProvider): LanguageService { const kafkaFileCodeLenses = new KafkaFileCodeLenses(producerLaunchStateProvider, consumerLaunchStateProvider, selectedClusterProvider); - const kafkaFileCompletion = new KafkaFileCompletion(); + const kafkaFileCompletion = new KafkaFileCompletion(selectedClusterProvider, topicProvider); return { parseKafkaFileDocument: (document: TextDocument) => parseKafkaFile(document), getCodeLenses: kafkaFileCodeLenses.getCodeLenses.bind(kafkaFileCodeLenses), diff --git a/src/kafka-file/languageservice/services/codeLensProvider.ts b/src/kafka-file/languageservice/services/codeLensProvider.ts index 74a2bc6..4f4fcb2 100644 --- a/src/kafka-file/languageservice/services/codeLensProvider.ts +++ b/src/kafka-file/languageservice/services/codeLensProvider.ts @@ -5,6 +5,9 @@ import { LaunchConsumerCommand, ProduceRecordCommand, ProduceRecordCommandHandle import { ProducerLaunchStateProvider, ConsumerLaunchStateProvider, SelectedClusterProvider } from "../kafkaFileLanguageService"; import { Block, BlockType, ConsumerBlock, KafkaFileDocument, ProducerBlock } from "../parser/kafkaFileParser"; +/** + * Kafka file codeLens support. + */ export class KafkaFileCodeLenses { constructor(private producerLaunchStateProvider: ProducerLaunchStateProvider, private consumerLaunchStateProvider: ConsumerLaunchStateProvider, private selectedClusterProvider: SelectedClusterProvider) { diff --git a/src/kafka-file/languageservice/services/completion.ts b/src/kafka-file/languageservice/services/completion.ts index defdcf6..a4ada38 100644 --- a/src/kafka-file/languageservice/services/completion.ts +++ b/src/kafka-file/languageservice/services/completion.ts @@ -1,15 +1,24 @@ import { TextDocument, Position, CompletionList, CompletionItem, SnippetString, MarkdownString, CompletionItemKind, Range } from "vscode"; +import { SelectedClusterProvider, TopicDetail, TopicProvider } from "../kafkaFileLanguageService"; import { consumerProperties, fakerjsAPI, ModelDefinition, producerProperties } from "../model"; import { Block, BlockType, Chunk, ConsumerBlock, KafkaFileDocument, MustacheExpression, NodeKind, ProducerBlock, Property } from "../parser/kafkaFileParser"; +/** + * Kafka file completion support. + */ export class KafkaFileCompletion { - doComplete(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): CompletionList | undefined { + constructor(private selectedClusterProvider: SelectedClusterProvider, private topicProvider: TopicProvider) { + + } + async doComplete(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): Promise { + // Get the AST node before the position where complation was triggered const node = kafkaFileDocument.findNodeBefore(position); if (!node) { return; } + // Following comments with use the '|' character to show the position where the complation is trigerred const items: Array = []; switch (node.kind) { case NodeKind.consumerBlock: { @@ -17,7 +26,7 @@ export class KafkaFileCompletion { // CONSUMER // | const lineRange = document.lineAt(position.line).range; - this.collectConsumerPropertyNames(undefined, lineRange, node, items); + await this.collectConsumerPropertyNames(undefined, lineRange, node, items); } break; } @@ -26,7 +35,7 @@ export class KafkaFileCompletion { // PRODUCER // | const lineRange = document.lineAt(position.line).range; - this.collectProducerPropertyNames(undefined, lineRange, node, items); + await this.collectProducerPropertyNames(undefined, lineRange, node, items); } break; } @@ -35,32 +44,17 @@ export class KafkaFileCompletion { const previous = new Position(position.line - 1, 1); const previousNode = kafkaFileDocument.findNodeBefore(previous); if (previousNode && previousNode.kind !== NodeKind.producerValue) { + // PRODUCER + // topic: abcd + // | + + // or + + // PRODUCER + // to|pic const lineRange = document.lineAt(position.line).range; const block = (previousNode.kind === NodeKind.producerBlock) ? previousNode : previousNode.parent; - this.collectProducerPropertyNames(undefined, lineRange, block, items); - } - break; - } - case NodeKind.propertyKey: { - const propertyKey = node; - const block = propertyKey.parent; - const lineRange = document.lineAt(position.line).range; - const propertyName = propertyKey.content; - if (block.type === BlockType.consumer) { - this.collectConsumerPropertyNames(propertyName, lineRange, block, items); - } else { - this.collectProducerPropertyNames(propertyName, lineRange, block, items); - } - break; - } - case NodeKind.propertyValue: { - const propertyValue = node; - const property = propertyValue.parent; - const block = propertyValue.parent; - if (block.type === BlockType.consumer) { - this.collectConsumerPropertyValues(propertyValue, property, block, items); - } else { - this.collectProducerPropertyValues(propertyValue, property, block, items); + await this.collectProducerPropertyNames(undefined, lineRange, block, items); } break; } @@ -71,48 +65,70 @@ export class KafkaFileCompletion { const propertyName = position.line === property.start.line ? property.propertyName : undefined; const lineRange = document.lineAt(position.line).range; if (block.type === BlockType.consumer) { - this.collectConsumerPropertyNames(propertyName, lineRange, block, items); + // CONSUMER + // key|: + + // or + + // CONSUMER + // key| + await this.collectConsumerPropertyNames(propertyName, lineRange, block, items); } else { - this.collectProducerPropertyNames(propertyName, lineRange, block, items); + // PRODUCER + // key|: + await this.collectProducerPropertyNames(propertyName, lineRange, block, items); } } else { const propertyValue = property.value; const expression = propertyValue?.findNodeBefore(position); if (expression && expression.kind === NodeKind.mustacheExpression) { - this.collectFakerJSExpression(expression, items); + // Completion was triggered inside a mustache expression which is inside the property value + + // PRODUCER + // key: abcd-{{|}} + this.collectFakerJSExpressions(expression, items); } else { const block = property.parent; if (block.type === BlockType.consumer) { - this.collectConsumerPropertyValues(propertyValue, property, block, items); + // CONSUMER + // key-format: | + await this.collectConsumerPropertyValues(propertyValue, property, block, items); } else { - this.collectProducerPropertyValues(propertyValue, property, block, items); + // PRODUCER + // key-format: | + await this.collectProducerPropertyValues(propertyValue, property, block, items); } } } break; } case NodeKind.mustacheExpression: { + // Completion was triggered inside a mustache expression which is inside the PRODUCER value + + // PRODUCER + // topic: abcd + // {{|}} const expression = node; - this.collectFakerJSExpression(expression, items); + this.collectFakerJSExpressions(expression, items); break; } } return new CompletionList(items, true); } - collectConsumerPropertyNames(propertyName: string | undefined, lineRange: Range, block: ConsumerBlock, items: Array) { - this.collectPropertyNames(propertyName, lineRange, block, consumerProperties, items); + async collectConsumerPropertyNames(propertyName: string | undefined, lineRange: Range, block: ConsumerBlock, items: Array) { + await this.collectPropertyNames(propertyName, lineRange, block, consumerProperties, items); } - collectProducerPropertyNames(propertyName: string | undefined, lineRange: Range, block: ProducerBlock, items: Array) { - this.collectPropertyNames(propertyName, lineRange, block, producerProperties, items); + async collectProducerPropertyNames(propertyName: string | undefined, lineRange: Range, block: ProducerBlock, items: Array) { + await this.collectPropertyNames(propertyName, lineRange, block, producerProperties, items); } - collectPropertyNames(propertyName: string | undefined, lineRange: Range, block: Block, metadata: ModelDefinition[], items: Array) { + async collectPropertyNames(propertyName: string | undefined, lineRange: Range, block: Block, metadata: ModelDefinition[], items: Array) { const existingProperties = block.properties .filter(property => property.key) .map(property => property.key?.content); - metadata.forEach((definition) => { + for (const definition of metadata) { const currentName = definition.name; if (existingProperties.indexOf(currentName) === -1 || propertyName === currentName) { const item = new CompletionItem(currentName); @@ -121,8 +137,9 @@ export class KafkaFileCompletion { item.documentation = new MarkdownString(definition.description); } const insertText = new SnippetString(`${currentName}: `); - if (definition.enum) { - insertText.appendChoice(definition.enum.map(item => item.name)); + const values = await this.getValues(definition); + if (values) { + insertText.appendChoice(values); } else { insertText.appendPlaceholder(currentName); } @@ -130,30 +147,36 @@ export class KafkaFileCompletion { item.range = lineRange; items.push(item); } - }); + }; } - collectConsumerPropertyValues(propertyValue: Chunk | undefined, property: Property, block: ConsumerBlock, items: Array) { + async collectConsumerPropertyValues(propertyValue: Chunk | undefined, property: Property, block: ConsumerBlock, items: Array) { const propertyName = property.propertyName; switch (propertyName) { case 'topic': - + // CONSUMER + // topic: | + await this.collectTopics(property, items); break; - default: + // CONSUMER + // key-format: | this.collectPropertyValues(propertyValue, property, block, consumerProperties, items); break; } } - collectProducerPropertyValues(propertyValue: Chunk | undefined, property: Property, block: ProducerBlock, items: Array) { + async collectProducerPropertyValues(propertyValue: Chunk | undefined, property: Property, block: ProducerBlock, items: Array) { const propertyName = property.propertyName; switch (propertyName) { case 'topic': - + // PRODUCER + // topic: | + await this.collectTopics(property, items); break; - default: + // PRODUCER + // key-format: | this.collectPropertyValues(propertyValue, property, block, producerProperties, items); break; } @@ -174,7 +197,6 @@ export class KafkaFileCompletion { if (definition.description) { item.documentation = new MarkdownString(definition.description); } - const insertText = new SnippetString(' '); insertText.appendText(value); item.insertText = insertText; @@ -183,7 +205,7 @@ export class KafkaFileCompletion { }); } - collectFakerJSExpression(expression: MustacheExpression, items: CompletionItem[]) { + collectFakerJSExpressions(expression: MustacheExpression, items: CompletionItem[]) { const expressionRange = expression.expressionRange; fakerjsAPI.forEach((definition) => { const value = definition.name; @@ -199,4 +221,56 @@ export class KafkaFileCompletion { items.push(item); }); } + + async collectTopics(property: Property, items: Array) { + const { clusterId } = this.selectedClusterProvider.getSelectedCluster(); + if (!clusterId) { + return; + } + + function createDocumentation(topic: TopicDetail): string { + return `Topic \`${topic.id}\`\n` + + ` * partition count: \`${topic.partitionCount}\`\n` + + ` * replication factor: \`${topic.replicationFactor}\`\n`; + } + const valueRange = property.propertyValueRange; + try { + const topics = await this.topicProvider.getTopics(clusterId); + topics.forEach((topic) => { + const value = topic.id; + const item = new CompletionItem(value); + item.kind = CompletionItemKind.Value; + item.documentation = new MarkdownString(createDocumentation(topic)); + const insertText = new SnippetString(' '); + insertText.appendText(value); + item.insertText = insertText; + item.range = valueRange; + items.push(item); + }); + } + catch (e) { + + } + } + + async getValues(definition: ModelDefinition): Promise { + if (definition.enum) { + return definition.enum.map(item => item.name); + } + if (definition.name === 'topic') { + // TODO : manage list of topics as choices, but how to handle when cluster is not available? + /*const { clusterId } = this.selectedClusterProvider.getSelectedCluster(); + if (clusterId) { + try { + const topics = await this.topicProvider.getTopics(clusterId); + if (topics.length > 0) { + return topics.map(item => item.id); + } + } + catch (e) { + return; + } + }*/ + } + } } diff --git a/src/test/suite/kafka-file/languageservice/codeLens.test.ts b/src/test/suite/kafka-file/languageservice/codeLens.test.ts index 805328f..88538c9 100644 --- a/src/test/suite/kafka-file/languageservice/codeLens.test.ts +++ b/src/test/suite/kafka-file/languageservice/codeLens.test.ts @@ -6,7 +6,7 @@ suite("Kafka File CodeLens Test Suite", () => { test("Empty blocks", async () => { const languageServiceConfig = new LanguageServiceConfig(); - const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); + const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); await assertCodeLens('', [], languageService); await assertCodeLens(' ', [], languageService); @@ -21,7 +21,7 @@ suite("Kafka File PRODUCER CodeLens Test Suite", () => { test("PRODUCER without cluster selection", async () => { const languageServiceConfig = new LanguageServiceConfig(); - const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); + const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); await assertCodeLens('PRODUCER', [ codeLens(position(0, 0), position(0, 0), { @@ -45,27 +45,27 @@ suite("Kafka File PRODUCER CodeLens Test Suite", () => { }) ], languageService); - await assertCodeLens( - 'PRODUCER\n' + - '### XXXXXXXXXXXXXXXXXXXXXXXX\n' + - 'PRODUCER', - [ - codeLens(position(0, 0), position(0, 0), { - command: 'vscode-kafka.explorer.selectcluster', - title: 'Select a cluster' - }), - codeLens(position(2, 0), position(2, 0), { - command: 'vscode-kafka.explorer.selectcluster', - title: 'Select a cluster' - }) - ], languageService); + await assertCodeLens( + 'PRODUCER\n' + + '### XXXXXXXXXXXXXXXXXXXXXXXX\n' + + 'PRODUCER', + [ + codeLens(position(0, 0), position(0, 0), { + command: 'vscode-kafka.explorer.selectcluster', + title: 'Select a cluster' + }), + codeLens(position(2, 0), position(2, 0), { + command: 'vscode-kafka.explorer.selectcluster', + title: 'Select a cluster' + }) + ], languageService); }); test("PRODUCER with cluster selection", async () => { const languageServiceConfig = new LanguageServiceConfig(); languageServiceConfig.setSelectedCluster({ clusterId: 'cluster1', clusterName: 'CLUSTER_1' }); - const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); + const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); await assertCodeLens('PRODUCER', [ codeLens(position(0, 0), position(0, 0), { @@ -109,8 +109,8 @@ suite("Kafka File PRODUCER CodeLens Test Suite", () => { 'key: a-key\n' + 'topic: abcd\n' + 'key-format: long\n' + - 'value-format: string\n' + - 'ABCD\n' + + 'value-format: string\n' + + 'ABCD\n' + 'EFGH', [ codeLens(position(0, 0), position(0, 0), { command: 'vscode-kafka.producer.produce', @@ -158,7 +158,7 @@ suite("Kafka File CONSUMER CodeLens Test Suite", () => { test("CONSUMER without cluster selection", async () => { const languageServiceConfig = new LanguageServiceConfig(); - const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); + const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); await assertCodeLens('CONSUMER group-1', [ codeLens(position(0, 0), position(0, 0), { @@ -187,7 +187,7 @@ suite("Kafka File CONSUMER CodeLens Test Suite", () => { const languageServiceConfig = new LanguageServiceConfig(); languageServiceConfig.setSelectedCluster({ clusterId: 'cluster1', clusterName: 'CLUSTER_1' }); languageServiceConfig.setConsumerLaunchState('cluster1', 'group-1', ConsumerLaunchState.started); - const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); + const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); await assertCodeLens('CONSUMER group-1', [ codeLens(position(0, 0), position(0, 0), { diff --git a/src/test/suite/kafka-file/languageservice/completionTopic.test.ts b/src/test/suite/kafka-file/languageservice/completionTopic.test.ts new file mode 100644 index 0000000..67b623a --- /dev/null +++ b/src/test/suite/kafka-file/languageservice/completionTopic.test.ts @@ -0,0 +1,64 @@ +import { CompletionItemKind } from "vscode"; +import { getLanguageService } from "../../../../kafka-file/languageservice/kafkaFileLanguageService"; +import { LanguageServiceConfig, position, range, testCompletion } from "./kafkaAssert"; + +const languageServiceConfig = new LanguageServiceConfig(); +languageServiceConfig.setSelectedCluster({ clusterId: 'cluster1', clusterName: 'CLUSTER_1' }); +languageServiceConfig.setTopics('cluster1', [{id : 'abcd', partitionCount : 1 , replicationFactor : 1}]); +const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); + +suite("Kafka File Completion with Topics Test Suite", () => { + + test("Empty completion", async () => { + await testCompletion('', { + items: [] + }, false, languageService); + + await testCompletion('ab|cd', { + items: [] + }, false, languageService); + + }); + +}); + +suite("Kafka File PRODUCER Topic Completion Test Suite", () => { + + test("PRODUCER Topic Completion", async () => { + + + await testCompletion( + 'PRODUCER a\n' + + 'topic: |' + , { + items: [ + { + label: 'abcd', kind: CompletionItemKind.Value, + insertText: ' abcd', + range: range(position(1, 6), position(1, 7)) + } + ] + }, false, languageService); + }); + +}); + +suite("Kafka File CONSUMER Topic Completion Test Suite", () => { + + test("CONSUMER Topic Completion", async () => { + + await testCompletion( + 'CONSUMER a\n' + + 'topic: |' + , { + items: [ + { + label: 'abcd', kind: CompletionItemKind.Value, + insertText: ' abcd', + range: range(position(1, 6), position(1, 7)) + } + ] + }, false, languageService); + }); + +}); diff --git a/src/test/suite/kafka-file/languageservice/kafkaAssert.ts b/src/test/suite/kafka-file/languageservice/kafkaAssert.ts index 0555e66..9e2e076 100644 --- a/src/test/suite/kafka-file/languageservice/kafkaAssert.ts +++ b/src/test/suite/kafka-file/languageservice/kafkaAssert.ts @@ -2,10 +2,10 @@ import * as assert from "assert"; import { CodeLens, Position, Range, Command, Uri, workspace, CompletionList, SnippetString } from "vscode"; import { ConsumerLaunchState } from "../../../../client"; import { ProducerLaunchState } from "../../../../client/producer"; -import { ConsumerLaunchStateProvider, getLanguageService, LanguageService, ProducerLaunchStateProvider, SelectedClusterProvider } from "../../../../kafka-file/languageservice/kafkaFileLanguageService"; +import { ConsumerLaunchStateProvider, getLanguageService, LanguageService, ProducerLaunchStateProvider, SelectedClusterProvider, TopicDetail, TopicProvider } from "../../../../kafka-file/languageservice/kafkaFileLanguageService"; import { BlockType, ProducerBlock } from "../../../../kafka-file/languageservice/parser/kafkaFileParser"; -export class LanguageServiceConfig implements ProducerLaunchStateProvider, ConsumerLaunchStateProvider, SelectedClusterProvider { +export class LanguageServiceConfig implements ProducerLaunchStateProvider, ConsumerLaunchStateProvider, SelectedClusterProvider, TopicProvider { private producerLaunchStates = new Map(); @@ -13,6 +13,7 @@ export class LanguageServiceConfig implements ProducerLaunchStateProvider, Consu private selectedCluster: { clusterId?: string, clusterName?: string } | undefined; + private topicsCache = new Map(); getProducerLaunchState(uri: Uri): ProducerLaunchState { const key = uri.toString(); const state = this.producerLaunchStates.get(key); @@ -49,10 +50,16 @@ export class LanguageServiceConfig implements ProducerLaunchStateProvider, Consu this.selectedCluster = selectedCluster; } + public setTopics(clusterId: string, topics: TopicDetail[]) { + this.topicsCache.set(clusterId, topics); + } + async getTopics(clusterId: string): Promise { + return this.topicsCache.get(clusterId) || []; + } } const languageServiceConfig = new LanguageServiceConfig(); -const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig); +const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig); export function getSimpleLanguageService() { return languageService; @@ -85,15 +92,14 @@ export async function assertCodeLens(content: string, expected: Array, } // Completion assert - -export async function testCompletion(value: string, expected: CompletionList, partial = false) { +export async function testCompletion(value: string, expected: CompletionList, partial = false, ls = languageService) { const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); let document = await getDocument(value); const position = document.positionAt(offset); - let ast = languageService.parseKafkaFileDocument(document); - const list = languageService.doComplete(document, ast, position); + let ast = ls.parseKafkaFileDocument(document); + const list = await ls.doComplete(document, ast, position); const items = list?.items; // no duplicate labels @@ -112,10 +118,10 @@ export async function testCompletion(value: string, expected: CompletionList, pa } expected.items.forEach((expectedItem, i) => { const actualItem = items[i]; - assert.deepStrictEqual(actualItem.label, expectedItem.label); - assert.deepStrictEqual(actualItem.kind, expectedItem.kind); - assert.deepStrictEqual((actualItem.insertText)?.value, expectedItem.insertText); - assert.deepStrictEqual(actualItem.range, expectedItem.range); + assert.deepStrictEqual(actualItem?.label, expectedItem.label); + assert.deepStrictEqual(actualItem?.kind, expectedItem.kind); + assert.deepStrictEqual((actualItem?.insertText)?.value, expectedItem.insertText); + assert.deepStrictEqual(actualItem?.range, expectedItem.range); }); } }