diff --git a/CHANGELOG.md b/CHANGELOG.md
index b043ebd6..d9e3cfcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to `Tools for Apache Kafka®` are documented in this file.
- Hide internal [strimzi](https://strimzi.io/) topics/consumers by default. See [#176](https://github.com/jlandersen/vscode-kafka/pull/176).
- Validation for available topics in `.kafka` files. See [#153](https://github.com/jlandersen/vscode-kafka/issues/153).
- Simplify snippets. See [#180](https://github.com/jlandersen/vscode-kafka/pull/180).
+- Hover support in `.kafka` files. See [#149](https://github.com/jlandersen/vscode-kafka/issues/149).
## [0.12.0] - 2021-04-26
### Added
diff --git a/docs/Consuming.md b/docs/Consuming.md
index 8be5cbc9..892da260 100644
--- a/docs/Consuming.md
+++ b/docs/Consuming.md
@@ -101,6 +101,13 @@ Existing topic validation is done only when cluster is `connected`. If the topic
![Existing topic validation](assets/kafka-file-consumer-topic-validation.png)
+#### Hover
+
+Hover for properties documentation and topic informations is available in .kafka files.
+
+Here is an example of hover on topic:
+
+![Existing topic validation](assets/kafka-file-consumer-topic-hover.png)
### Start Consumer command
diff --git a/docs/Producing.md b/docs/Producing.md
index 3634b8f6..a865224d 100644
--- a/docs/Producing.md
+++ b/docs/Producing.md
@@ -83,6 +83,14 @@ Validation will help you write valid producers in .kafka files.
![Existing topic validation](assets/kafka-file-producer-topic-validation.png)
+### Hover
+
+Hover for properties documentation and topic informations is available in .kafka files.
+
+Here is an example of hover on topic:
+
+![Existing topic validation](assets/kafka-file-producer-topic-hover.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-hover.png b/docs/assets/kafka-file-consumer-topic-hover.png
new file mode 100644
index 00000000..c4cf4823
Binary files /dev/null and b/docs/assets/kafka-file-consumer-topic-hover.png differ
diff --git a/docs/assets/kafka-file-producer-topic-hover.png b/docs/assets/kafka-file-producer-topic-hover.png
new file mode 100644
index 00000000..d564dce6
Binary files /dev/null and b/docs/assets/kafka-file-producer-topic-hover.png differ
diff --git a/src/docs/markdownPreviewProvider.ts b/src/docs/markdownPreviewProvider.ts
index 71cc61df..523cacba 100644
--- a/src/docs/markdownPreviewProvider.ts
+++ b/src/docs/markdownPreviewProvider.ts
@@ -62,7 +62,7 @@ class MarkdownPreviewProvider implements Disposable {
return `${linkText}`;
});
body = await commands.executeCommand(MARKDOWN_API_RENDER, markdownString);
- if(body !== undefined) {
+ if (body !== undefined) {
this.documentCache.set(markdownFilePath, body);
}
}
@@ -120,3 +120,15 @@ class MarkdownPreviewProvider implements Disposable {
}
export const markdownPreviewProvider: MarkdownPreviewProvider = new MarkdownPreviewProvider();
+
+export type EmbeddedPage = "Consuming" | "Producing";
+
+type ConsumingSection = "deserializer" | "kafka-file";
+
+type ProducingSection = "serializer" | "kafka-file" | "randomized-content";
+
+export type EmbeddedSection = ConsumingSection | ProducingSection;
+
+export function getDocumentationPageUri(page: EmbeddedPage, section: EmbeddedSection) {
+ return `command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22${page}%22%2C%22section%22%3A%22${section}%22%7D%5D`;
+}
diff --git a/src/kafka-file/kafkaFileClient.ts b/src/kafka-file/kafkaFileClient.ts
index 890e2333..39852fc0 100644
--- a/src/kafka-file/kafkaFileClient.ts
+++ b/src/kafka-file/kafkaFileClient.ts
@@ -164,6 +164,12 @@ export function startLanguageClient(
const diagnostics = new KafkaFileDiagnostics(kafkaFileDocuments, languageService, clusterSettings, clientAccessor, modelProvider, workspaceSettings);
context.subscriptions.push(diagnostics);
+ // Hover
+ const hover = new KafkaFileHoverProvider(kafkaFileDocuments, languageService);
+ context.subscriptions.push(
+ vscode.languages.registerHoverProvider(documentSelector, hover)
+ );
+
// Open / Close document
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(e => {
if (e.languageId === 'kafka') {
@@ -277,7 +283,7 @@ class KafkaFileCompletionItemProvider extends AbstractKafkaFileFeature implement
return runSafeAsync(async () => {
const kafkaFileDocument = this.getKafkaFileDocument(document);
return this.languageService.doComplete(document, kafkaFileDocument, this.workspaceSettings.producerFakerJSEnabled, position);
- }, new vscode.CompletionList(), `Error while computing code lenses for ${document.uri}`, token);
+ }, new vscode.CompletionList(), `Error while computing completion for ${document.uri}`, token);
}
}
@@ -359,3 +365,13 @@ class KafkaFileDiagnostics extends AbstractKafkaFileFeature implements vscode.Di
this.diagnosticCollection.dispose();
}
}
+
+class KafkaFileHoverProvider extends AbstractKafkaFileFeature implements vscode.HoverProvider {
+ provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult {
+ return runSafeAsync(async () => {
+ const kafkaFileDocument = this.getKafkaFileDocument(document);
+ return this.languageService.doHover(document, kafkaFileDocument, position);
+ }, null, `Error while computing hover for ${document.uri}`, token);
+ }
+
+}
\ No newline at end of file
diff --git a/src/kafka-file/languageservice/kafkaFileLanguageService.ts b/src/kafka-file/languageservice/kafkaFileLanguageService.ts
index e88f5535..cf27cfb4 100644
--- a/src/kafka-file/languageservice/kafkaFileLanguageService.ts
+++ b/src/kafka-file/languageservice/kafkaFileLanguageService.ts
@@ -1,4 +1,4 @@
-import { CodeLens, CompletionList, Diagnostic, Position, TextDocument, Uri } from "vscode";
+import { CodeLens, CompletionList, Diagnostic, Hover, Position, TextDocument, Uri } from "vscode";
import { ClientState, ConsumerLaunchState } from "../../client";
import { BrokerConfigs } from "../../client/config";
import { ProducerLaunchState } from "../../client/producer";
@@ -6,6 +6,7 @@ import { KafkaFileDocument, parseKafkaFile } from "./parser/kafkaFileParser";
import { KafkaFileCodeLenses } from "./services/codeLensProvider";
import { KafkaFileCompletion } from "./services/completion";
import { KafkaFileDiagnostics } from "./services/diagnostics";
+import { KafkaFileHover } from "./services/hover";
/**
* Provider API which gets the state for a given producer.
@@ -49,6 +50,7 @@ export interface TopicProvider {
*
*/
export interface LanguageService {
+
/**
* Parse the given text document and returns an AST.
*
@@ -85,6 +87,15 @@ export interface LanguageService {
* @param kafkaFileDocument the parsed AST.
*/
doDiagnostics(document: TextDocument, kafkaFileDocument: KafkaFileDocument, producerFakerJSEnabled: boolean): Promise;
+
+ /**
+ * Returns the hover 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 hover was triggered.
+ */
+ doHover(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): Promise;
}
/**
@@ -100,10 +111,18 @@ export function getLanguageService(producerLaunchStateProvider: ProducerLaunchSt
const codeLenses = new KafkaFileCodeLenses(producerLaunchStateProvider, consumerLaunchStateProvider, selectedClusterProvider);
const completion = new KafkaFileCompletion(selectedClusterProvider, topicProvider);
const diagnostics = new KafkaFileDiagnostics(selectedClusterProvider, topicProvider);
+ const hover = new KafkaFileHover(selectedClusterProvider, topicProvider);
return {
parseKafkaFileDocument: (document: TextDocument) => parseKafkaFile(document),
getCodeLenses: codeLenses.getCodeLenses.bind(codeLenses),
doComplete: completion.doComplete.bind(completion),
- doDiagnostics: diagnostics.doDiagnostics.bind(diagnostics)
+ doDiagnostics: diagnostics.doDiagnostics.bind(diagnostics),
+ doHover: hover.doHover.bind(hover)
};
}
+
+export function createTopicDocumentation(topic: TopicDetail): string {
+ return `Topic \`${topic.id}\`\n` +
+ ` * partition count: \`${topic.partitionCount}\`\n` +
+ ` * replication factor: \`${topic.replicationFactor}\`\n`;
+}
\ No newline at end of file
diff --git a/src/kafka-file/languageservice/model.ts b/src/kafka-file/languageservice/model.ts
index 360fe7b1..caef9998 100644
--- a/src/kafka-file/languageservice/model.ts
+++ b/src/kafka-file/languageservice/model.ts
@@ -1,3 +1,5 @@
+import { getDocumentationPageUri } from "../../docs/markdownPreviewProvider";
+
export class Model {
private cache = new Map();
@@ -16,18 +18,22 @@ export class Model {
}
public hasDefinitionEnum(name: string, value: string): boolean {
+ return this.getDefinitionEnum(name, value) !== undefined;
+ }
+
+ public getDefinitionEnum(name: string, value: string): ModelDefinition | undefined {
const definition = this.getDefinition(name);
if (!definition) {
- return false;
+ return undefined;
}
if (definition.enum) {
for (const item of definition.enum) {
if (item.name === value) {
- return true;
+ return item;
}
}
}
- return false;
+ return undefined;
}
}
@@ -59,7 +65,7 @@ const consumerProperties = [
},
{
name: "key-format",
- description: "[Deserializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Consuming.md#Deserializer) to use for the key *[optional]*.",
+ description: `[Deserializer](${getDocumentationPageUri('Consuming', 'deserializer')}) to use for the key *[optional]*.`,
enum: [
{
name: "none",
@@ -93,7 +99,7 @@ const consumerProperties = [
},
{
name: "value-format",
- description: "[Deserializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Consuming.md#Deserializer) to use for the value *[optional]*.",
+ description: `[Deserializer](${getDocumentationPageUri('Consuming', 'deserializer')}) to use for the value *[optional]*.`,
enum: [
{
name: "none",
@@ -148,7 +154,7 @@ const producerProperties = [
},
{
name: "key-format",
- description: "[Serializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#Serializer) to use for the key *[optional]*.",
+ description: `[Serializer](${getDocumentationPageUri('Producing', 'serializer')}) to use for the key *[optional]*.`,
enum: [
{
name: "string",
@@ -178,7 +184,7 @@ const producerProperties = [
},
{
name: "value-format",
- description: "[Serializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#Serializer) to use for the value *[optional]*.",
+ description: `[Deserializer](${getDocumentationPageUri('Producing', 'serializer')}) to use for the value *[optional]*.`,
enum: [
{
name: "string",
@@ -381,18 +387,18 @@ const fakerjsAPI = [
] as ModelDefinition[];
export interface PartModelProvider {
- getPart(name: string) : PartModelProvider | undefined;
+ getPart(name: string): PartModelProvider | undefined;
}
-class PartModel implements PartModelProvider{
+class PartModel implements PartModelProvider {
private cache = new Map();
- getPart(name: string) : PartModelProvider | undefined {
+ getPart(name: string): PartModelProvider | undefined {
return this.cache.get(name);
}
- getOrCreate(name: string) : PartModelProvider {
+ getOrCreate(name: string): PartModelProvider {
let part = this.getPart(name);
if (!part) {
part = new PartModel();
@@ -411,12 +417,12 @@ class FakerJSModel extends Model implements PartModelProvider {
const parts = definition.name.split('.');
let partModel = this.root;
parts.forEach(part => {
- partModel = partModel.getOrCreate(part);
+ partModel = partModel.getOrCreate(part);
});
});
}
- getPart(name: string) : PartModelProvider | undefined {
+ getPart(name: string): PartModelProvider | undefined {
return this.root.getPart(name);
}
}
diff --git a/src/kafka-file/languageservice/parser/kafkaFileParser.ts b/src/kafka-file/languageservice/parser/kafkaFileParser.ts
index b759dab1..db3fc849 100644
--- a/src/kafka-file/languageservice/parser/kafkaFileParser.ts
+++ b/src/kafka-file/languageservice/parser/kafkaFileParser.ts
@@ -17,7 +17,9 @@ export enum NodeKind {
export interface Node {
start: Position;
end: Position;
+ range(): Range;
findNodeBefore(offset: Position): Node;
+ findNodeAt(offset: Position): Node;
lastChild: Node | undefined;
parent: Node | undefined;
kind: NodeKind;
@@ -32,10 +34,20 @@ class BaseNode implements Node {
}
+ public range(): Range {
+ const start = this.start;
+ const end = this.end;
+ return new Range(start, end);
+ }
+
public findNodeBefore(offset: Position): Node {
return this;
}
+ public findNodeAt(offset: Position): Node {
+ return this;
+ }
+
public get lastChild(): Node | undefined { return undefined; }
}
@@ -65,6 +77,17 @@ class ChildrenNode extends BaseNode {
return this;
}
+ public findNodeAt(offset: Position): Node {
+ const idx = findFirst(this.children, c => offset.isBeforeOrEqual(c.start)) - 1;
+ if (idx >= 0) {
+ const child = this.children[idx];
+ if (offset.isAfter(child.start) && offset.isBeforeOrEqual(child.end)) {
+ return child.findNodeAt(offset);
+ }
+ }
+ return this;
+ }
+
public get lastChild(): Node | undefined { return this.children.length ? this.children[this.children.length - 1] : void 0; };
}
@@ -84,6 +107,7 @@ export class Chunk extends BaseNode {
constructor(public readonly content: string, start: Position, end: Position, kind: NodeKind) {
super(start, end, kind);
}
+
}
export class Property extends BaseNode {
@@ -106,12 +130,6 @@ export class Property extends BaseNode {
return this.value?.content.trim();
}
- public get propertyRange(): Range {
- const start = this.start;
- const end = this.end;
- return new Range(start, end);
- }
-
public get propertyKeyRange(): Range {
const start = this.start;
const end = this.assignerCharacter ? new Position(this.start.line, this.assignerCharacter) : this.end;
@@ -159,6 +177,13 @@ export class Property extends BaseNode {
}
return true;
}
+
+ findNodeAt(position : Position) : Node {
+ if (this.isBeforeAssigner(position)) {
+ return this.key?.findNodeAt(position) || this;
+ }
+ return this.value?.findNodeAt(position) || this;
+ }
}
export abstract class Block extends ChildrenNode {
@@ -175,7 +200,7 @@ export abstract class Block extends ChildrenNode {
getPropertyValue(name: string): string | undefined {
const property = this.getProperty(name);
- return property?.value?.content;
+ return property?.propertyValue;
}
getProperty(name: string): Property | undefined {
diff --git a/src/kafka-file/languageservice/services/completion.ts b/src/kafka-file/languageservice/services/completion.ts
index 2cd2a90b..36aec62a 100644
--- a/src/kafka-file/languageservice/services/completion.ts
+++ b/src/kafka-file/languageservice/services/completion.ts
@@ -1,5 +1,5 @@
import { TextDocument, Position, CompletionList, CompletionItem, SnippetString, MarkdownString, CompletionItemKind, Range } from "vscode";
-import { SelectedClusterProvider, TopicDetail, TopicProvider } from "../kafkaFileLanguageService";
+import { createTopicDocumentation, SelectedClusterProvider, TopicProvider } from "../kafkaFileLanguageService";
import { consumerModel, fakerjsAPIModel, Model, ModelDefinition, producerModel } from "../model";
import { Block, BlockType, Chunk, ConsumerBlock, KafkaFileDocument, MustacheExpression, NodeKind, ProducerBlock, Property } from "../parser/kafkaFileParser";
@@ -134,7 +134,7 @@ export class KafkaFileCompletion {
const item = new CompletionItem(currentName);
item.kind = CompletionItemKind.Property;
if (definition.description) {
- item.documentation = new MarkdownString(definition.description);
+ item.documentation = createMarkdownString(definition.description);
}
const insertText = new SnippetString(`${currentName}: `);
const values = await this.getValues(definition);
@@ -198,7 +198,7 @@ export class KafkaFileCompletion {
const item = new CompletionItem(value);
item.kind = CompletionItemKind.Value;
if (definition.description) {
- item.documentation = new MarkdownString(definition.description);
+ item.documentation = createMarkdownString(definition.description);
}
const insertText = new SnippetString(' ');
insertText.appendText(value);
@@ -219,7 +219,7 @@ export class KafkaFileCompletion {
const item = new CompletionItem(value);
item.kind = CompletionItemKind.Variable;
if (definition.description) {
- item.documentation = new MarkdownString(definition.description);
+ item.documentation = createMarkdownString(definition.description);
}
const insertText = new SnippetString('');
insertText.appendText(value);
@@ -235,11 +235,6 @@ export class KafkaFileCompletion {
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);
@@ -247,7 +242,7 @@ export class KafkaFileCompletion {
const value = topic.id;
const item = new CompletionItem(value);
item.kind = CompletionItemKind.Value;
- item.documentation = new MarkdownString(createDocumentation(topic));
+ item.documentation = new MarkdownString(createTopicDocumentation(topic));
const insertText = new SnippetString(' ');
insertText.appendText(value);
item.insertText = insertText;
@@ -281,3 +276,9 @@ export class KafkaFileCompletion {
}
}
}
+
+function createMarkdownString(contents : string) {
+ const doc = new MarkdownString(contents);
+ doc.isTrusted = true;
+ return doc;
+}
\ No newline at end of file
diff --git a/src/kafka-file/languageservice/services/diagnostics.ts b/src/kafka-file/languageservice/services/diagnostics.ts
index d05a4135..94e07152 100644
--- a/src/kafka-file/languageservice/services/diagnostics.ts
+++ b/src/kafka-file/languageservice/services/diagnostics.ts
@@ -208,14 +208,14 @@ export class KafkaFileDiagnostics {
const assigner = property.assignerCharacter;
if (!assigner) {
// Error => topic
- const range = property.propertyRange;
+ const range = property.range();
diagnostics.push(new Diagnostic(range, `Missing ':' sign after '${propertyName}'`, DiagnosticSeverity.Error));
return;
}
// 1.2. property must declare a key
if (!propertyName) {
// Error => :string
- const range = property.propertyRange;
+ const range = property.range();
diagnostics.push(new Diagnostic(range, "Property must define a name before ':' sign", DiagnosticSeverity.Error));
return;
}
@@ -280,7 +280,7 @@ export class KafkaFileDiagnostics {
// The topic validation is done, only when the cluster is connected
if (!await this.topicProvider.getTopic(clusterId, topicId)) {
// The topic doesn't exist, report an error
- const range = topicProperty.propertyTrimmedValueRange || topicProperty.propertyRange;
+ const range = topicProperty.propertyTrimmedValueRange || topicProperty.range();
const autoCreate = await this.topicProvider.getAutoCreateTopicEnabled(clusterId);
const errorMessage = getTopicErrorMessage(topicId, autoCreate, blockType);
const severity = getTopicErrorSeverity(autoCreate);
diff --git a/src/kafka-file/languageservice/services/hover.ts b/src/kafka-file/languageservice/services/hover.ts
new file mode 100644
index 00000000..36218c71
--- /dev/null
+++ b/src/kafka-file/languageservice/services/hover.ts
@@ -0,0 +1,170 @@
+import { Hover, MarkdownString, Position, Range, TextDocument } from "vscode";
+import { getDocumentationPageUri } from "../../../docs/markdownPreviewProvider";
+import { createTopicDocumentation, SelectedClusterProvider, TopicProvider } from "../kafkaFileLanguageService";
+import { consumerModel, Model, producerModel } from "../model";
+import { Block, BlockType, Chunk, ConsumerBlock, KafkaFileDocument, MustacheExpression, NodeKind, ProducerBlock, Property } from "../parser/kafkaFileParser";
+
+export class KafkaFileHover {
+
+ constructor(private selectedClusterProvider: SelectedClusterProvider, private topicProvider: TopicProvider) {
+
+ }
+
+ async doHover(document: TextDocument, kafkaFileDocument: KafkaFileDocument, position: Position): Promise {
+ // Get the AST node before the position where complation was triggered
+ const node = kafkaFileDocument.findNodeAt(position);
+ if (!node) {
+ return;
+ }
+ switch (node.kind) {
+
+ case NodeKind.consumerBlock: {
+ const block = node;
+ const topic = block.getPropertyValue('topic');
+ return createHover(`Consumer declaration${topic ? ` for topic \`${topic}\`` : ''}.\n\nSee [here](${getDocumentationPageUri('Consuming', 'kafka-file')}) for more informations.`, node.range());
+ }
+
+ case NodeKind.producerBlock: {
+ const block = node;
+ const topic = block.getPropertyValue('topic');
+ return createHover(`Producer declaration${topic ? ` for topic \`${topic}\`` : ''}.\n\nSee [here](${getDocumentationPageUri('Producing', 'kafka-file')}) for more informations.`, node.range());
+ }
+
+ case NodeKind.propertyKey: {
+ const propertyKey = node;
+ const property = propertyKey.parent;
+ const propertyName = propertyKey.content;
+ const propertyKeyRange = propertyKey.range();
+ const block = property.parent;
+ if (block.type === BlockType.consumer) {
+ // CONSUMER
+ // key|:
+
+ // or
+
+ // CONSUMER
+ // key|
+ return await this.getHoverForConsumerPropertyNames(propertyName, propertyKeyRange, block);
+ } else {
+ // PRODUCER
+ // key|:
+ return await this.getHoverForProducerPropertyNames(propertyName, propertyKeyRange, block);
+ }
+ }
+
+ case NodeKind.propertyValue: {
+ const propertyValue = node;
+ const property = propertyValue.parent;
+ const block = property.parent;
+ if (block.type === BlockType.consumer) {
+ // CONSUMER
+ // key-format: |
+ return await this.getHoverForConsumerPropertyValues(propertyValue, property, block);
+ } else {
+ // PRODUCER
+ // key-format: |
+ return await this.getHoverForProducerPropertyValues(propertyValue, property, block);
+ }
+ }
+
+ case NodeKind.mustacheExpression: {
+ const expression = node;
+ return createHover(`FakerJS expression.\n\nSee [here](${getDocumentationPageUri('Producing', 'randomized-content')}) for more informations.`, expression.enclosedExpressionRange);
+ }
+
+ case NodeKind.producerValue: {
+ return createHover(`Producer value.\n\nSee [here](${getDocumentationPageUri('Producing', 'kafka-file')}) for more informations.`, node.range());
+ }
+ }
+ }
+
+ async getHoverForConsumerPropertyNames(propertyName: string, propertyKeyRange: Range, block: ConsumerBlock): Promise {
+ return await this.getHoverForPropertyNames(propertyName, propertyKeyRange, block, consumerModel);
+ }
+
+ async getHoverForProducerPropertyNames(propertyName: string, propertyKeyRange: Range, block: ProducerBlock): Promise {
+ return await this.getHoverForPropertyNames(propertyName, propertyKeyRange, block, producerModel);
+ }
+
+ async getHoverForPropertyNames(propertyName: string, propertyKeyRange: Range, block: Block, metadata: Model): Promise {
+ const definition = metadata.getDefinition(propertyName);
+ if (definition && definition.description) {
+ return createHover(definition.description, propertyKeyRange);
+ }
+ }
+
+ async getHoverForConsumerPropertyValues(propertyValue: Chunk, property: Property, block: ConsumerBlock): Promise {
+ const propertyName = property.propertyName;
+ switch (propertyName) {
+ case 'topic':
+ // CONSUMER
+ // topic: |
+ return await this.getHoverForTopic(property);
+ default:
+ // CONSUMER
+ // key-format: |
+ return await this.getHoverForPropertyValues(propertyValue, property, block, consumerModel);
+ }
+ }
+
+
+ async getHoverForProducerPropertyValues(propertyValue: Chunk, property: Property, block: ProducerBlock): Promise {
+ const propertyName = property.propertyName;
+ switch (propertyName) {
+ case 'topic':
+ // PRODUCER
+ // topic: |
+ return await this.getHoverForTopic(property);
+ default:
+ // PRODUCER
+ // key-format: |
+ return await this.getHoverForPropertyValues(propertyValue, property, block, producerModel);
+ }
+ }
+
+ async getHoverForTopic(property: Property): Promise {
+ const propertyValue = property.value;
+ if (!propertyValue) {
+ return;
+ }
+ const { clusterId } = this.selectedClusterProvider.getSelectedCluster();
+ if (!clusterId) {
+ return;
+ }
+
+ try {
+ const topicId = propertyValue.content.trim();
+ const topics = await this.topicProvider.getTopics(clusterId);
+ if (topics.length > 0) {
+ const topic = topics
+ .find(t => t.id === topicId);
+ if (topic) {
+ return createHover(createTopicDocumentation(topic), propertyValue.range());
+ }
+ }
+ }
+ catch (e) {
+ return;
+ }
+
+ return undefined;
+ }
+
+ async getHoverForPropertyValues(propertyValue: Chunk, property: Property, block: Block, metadata: Model): Promise {
+ const propertyName = property.propertyName;
+ if (!propertyName) {
+ return;
+ }
+ const definition = metadata.getDefinitionEnum(propertyName, propertyValue.content.trim());
+ if (definition && definition.description) {
+ return createHover(definition.description, property.propertyTrimmedValueRange);
+ }
+ return undefined;
+ }
+}
+
+function createHover(contents: string, range?: Range): Hover {
+ const doc = new MarkdownString(contents);
+ doc.isTrusted = true;
+ return new Hover(doc, range);
+}
\ No newline at end of file
diff --git a/src/test/suite/kafka-file/languageservice/hover.test.ts b/src/test/suite/kafka-file/languageservice/hover.test.ts
new file mode 100644
index 00000000..8f0bd10e
--- /dev/null
+++ b/src/test/suite/kafka-file/languageservice/hover.test.ts
@@ -0,0 +1,310 @@
+import { ClientState } from "../../../../client";
+import { getLanguageService } from "../../../../kafka-file/languageservice/kafkaFileLanguageService";
+import { assertHover, hover, LanguageServiceConfig, position } from "./kafkaAssert";
+
+suite("Kafka File Hover Test Suite", () => {
+
+ test("Empty hover", async () => {
+ await assertHover('');
+
+ await assertHover('ab|cd');
+
+ });
+
+});
+
+suite("Kafka File CONSUMER Hover Test Suite", () => {
+
+ test("CONSUMER declaration no topic Hover", async () => {
+
+ await assertHover(
+ 'CONS|UMER\n',
+ hover(
+ `Consumer declaration.\n\nSee [here](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Consuming%22%2C%22section%22%3A%22kafka-file%22%7D%5D) for more informations.`,
+ position(0, 0),
+ position(1, 0)
+ )
+ );
+
+ });
+
+ test("CONSUMER declaration with topic Hover", async () => {
+
+ await assertHover(
+ 'CONS|UMER\n' +
+ 'topic: abcd',
+ hover(
+ `Consumer declaration for topic \`abcd\`.\n\n\See [here](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Consuming%22%2C%22section%22%3A%22kafka-file%22%7D%5D) for more informations.`,
+ position(0, 0),
+ position(1, 11)
+ )
+ );
+
+ });
+
+ test("topic property name Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'top|ic: abcd',
+ hover(
+ `The topic id *[required]*`,
+ position(1, 0),
+ position(1, 5)
+ )
+ );
+
+ });
+
+ test("topic property value Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'topic: ab|cd'
+ );
+
+ const languageServiceConfig = new LanguageServiceConfig();
+ languageServiceConfig.setTopics('cluster1', [{ id: 'abcd', partitionCount: 1, replicationFactor: 1 }]);
+ const connectedCuster = { clusterId: 'cluster1', clusterName: 'CLUSTER_1', clusterState: ClientState.connected };
+ languageServiceConfig.setSelectedCluster(connectedCuster);
+ const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig);
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'topic: ab|cd',
+ hover(
+ 'Topic `abcd`\n * partition count: `1`\n * replication factor: `1`\n',
+ position(1, 6),
+ position(1, 11)
+ ),
+ languageService
+ );
+
+ });
+
+ test("from property name Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'fro|m: earliest',
+ hover(
+ 'The offset from which the consumer group will start consuming messages from. Possible values are: `earliest`, `latest`, or an integer value. *[optional]*.',
+ position(1, 0),
+ position(1, 4)
+ )
+ );
+
+ });
+
+ test("key-format property name Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'key-for|mat: string',
+ hover(
+ '[Deserializer](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Consuming%22%2C%22section%22%3A%22deserializer%22%7D%5D) to use for the key *[optional]*.',
+ position(1, 0),
+ position(1, 10)
+ )
+ );
+
+ });
+
+ test("key-format property value Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'key-format: stri|ng',
+ hover(
+ 'Similar deserializer to the Kafka Java client [org.apache.kafka.common.serialization.StringDeserializer](https://github.com/apache/kafka/blob/master/clients/src/main/java/org/apache/kafka/common/serialization/StringDeserializer.java) which currently only supports `UTF-8` encoding.',
+ position(1, 12),
+ position(1, 18)
+ )
+ );
+
+ });
+
+ test("value-format property name Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'value-for|mat: string',
+ hover(
+ '[Deserializer](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Consuming%22%2C%22section%22%3A%22deserializer%22%7D%5D) to use for the value *[optional]*.',
+ position(1, 0),
+ position(1, 12)
+ )
+ );
+
+ });
+
+ test("value-format property value Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'value-format: stri|ng',
+ hover(
+ 'Similar deserializer to the Kafka Java client [org.apache.kafka.common.serialization.StringDeserializer](https://github.com/apache/kafka/blob/master/clients/src/main/java/org/apache/kafka/common/serialization/StringDeserializer.java) which currently only supports `UTF-8` encoding.',
+ position(1, 14),
+ position(1, 20)
+ )
+ );
+
+ });
+
+ test("partitions property name Hover", async () => {
+
+ await assertHover(
+ 'CONSUMER\n' +
+ 'partition|s: 0',
+ hover(
+ 'the partition number(s), or a partitions range, or a combinaison of partitions ranges *[optional]*. eg:\n* 0\n* 0,1,2\n* 0-2\n* 0,2-3',
+ position(1, 0),
+ position(1, 10)
+ )
+ );
+
+ });
+
+});
+
+suite("Kafka File PRODUCER Hover Test Suite", () => {
+
+ test("PRODUCER declaration no topic Hover", async () => {
+
+ await assertHover(
+ 'PRODU|CER\n',
+ hover(
+ `See [here](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Producing%22%2C%22section%22%3A%22kafka-file%22%7D%5D) for more informations.`,
+ position(0, 0),
+ position(1, 0)
+ )
+ );
+
+ });
+
+ test("PRODUCER declaration with topic Hover", async () => {
+
+ await assertHover(
+ 'PRODU|CER\n' +
+ 'topic: abcd',
+ hover(
+ `Producer declaration for topic \`abcd\`.\n\n\See [here](command:vscode-kafka.open.docs.page?%5B%7B%22page%22%3A%22Producing%22%2C%22section%22%3A%22kafka-file%22%7D%5D) for more informations.`,
+ position(0, 0),
+ position(1, 11)
+ )
+ );
+
+ test("topic property name Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'top|ic: abcd',
+ hover(
+ `The topic id *[required]*`,
+ position(1, 0),
+ position(1, 5)
+ )
+ );
+
+ });
+
+ test("topic property value Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'topic: ab|cd'
+ );
+
+ const languageServiceConfig = new LanguageServiceConfig();
+ languageServiceConfig.setTopics('cluster1', [{ id: 'abcd', partitionCount: 1, replicationFactor: 1 }]);
+ const connectedCuster = { clusterId: 'cluster1', clusterName: 'CLUSTER_1', clusterState: ClientState.connected };
+ languageServiceConfig.setSelectedCluster(connectedCuster);
+ const languageService = getLanguageService(languageServiceConfig, languageServiceConfig, languageServiceConfig, languageServiceConfig);
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'topic: ab|cd',
+ hover(
+ 'Topic `abcd`\n * partition count: `1`\n * replication factor: `1`\n',
+ position(1, 6),
+ position(1, 11)
+ ),
+ languageService
+ );
+
+ });
+
+ test("key property name Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'ke|y: abcd',
+ hover(
+ 'The key *[optional]*.',
+ position(1, 0),
+ position(1, 3)
+ )
+ );
+
+ });
+
+ test("key-format property name Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'key-for|mat: string',
+ hover(
+ '[Serializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#Serializer) to use for the key *[optional]*.',
+ position(1, 0),
+ position(1, 10)
+ )
+ );
+
+ });
+
+ test("key-format property value Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'key-format: stri|ng',
+ hover(
+ 'Similar serializer to the Kafka Java client [org.apache.kafka.common.serialization.StringDeserializer](https://github.com/apache/kafka/blob/master/clients/src/main/java/org/apache/kafka/common/serialization/StringSerializer.java) which currently only supports `UTF-8` encoding.',
+ position(1, 12),
+ position(1, 18)
+ )
+ );
+
+ });
+
+ test("value-format property name Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'value-for|mat: string',
+ hover(
+ '[Serializer](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#Serializer) to use for the value *[optional]*.',
+ position(1, 0),
+ position(1, 12)
+ )
+ );
+
+ });
+
+ test("value-format property value Hover", async () => {
+
+ await assertHover(
+ 'PRODUCER\n' +
+ 'value-format: stri|ng',
+ hover(
+ 'Similar serializer to the Kafka Java client [org.apache.kafka.common.serialization.StringSerializer](https://github.com/apache/kafka/blob/master/clients/src/main/java/org/apache/kafka/common/serialization/Seserializer.java) which currently only supports `UTF-8` encoding.',
+ position(1, 14),
+ position(1, 20)
+ )
+ );
+
+ });
+
+ });
+
+});
\ No newline at end of file
diff --git a/src/test/suite/kafka-file/languageservice/kafkaAssert.ts b/src/test/suite/kafka-file/languageservice/kafkaAssert.ts
index 8d7f0c0d..976de9db 100644
--- a/src/test/suite/kafka-file/languageservice/kafkaAssert.ts
+++ b/src/test/suite/kafka-file/languageservice/kafkaAssert.ts
@@ -1,5 +1,5 @@
import * as assert from "assert";
-import { CodeLens, Position, Range, Command, Uri, workspace, CompletionList, SnippetString, Diagnostic, DiagnosticSeverity } from "vscode";
+import { CodeLens, Position, Range, Command, Uri, workspace, CompletionList, SnippetString, Diagnostic, DiagnosticSeverity, Hover, MarkdownString } from "vscode";
import { ClientState, ConsumerLaunchState } from "../../../../client";
import { BrokerConfigs } from "../../../../client/config";
import { ProducerLaunchState } from "../../../../client/producer";
@@ -12,7 +12,7 @@ export class LanguageServiceConfig implements ProducerLaunchStateProvider, Consu
private consumerLaunchStates = new Map();
- private selectedCluster: { clusterId?: string, clusterName?: string, clusterState? : ClientState } | undefined;
+ private selectedCluster: { clusterId?: string, clusterName?: string, clusterState?: ClientState } | undefined;
private topicsCache = new Map();
@@ -50,7 +50,7 @@ export class LanguageServiceConfig implements ProducerLaunchStateProvider, Consu
return {};
}
- public setSelectedCluster(selectedCluster: { clusterId?: string, clusterName?: string, clusterState? : ClientState }) {
+ public setSelectedCluster(selectedCluster: { clusterId?: string, clusterName?: string, clusterState?: ClientState }) {
this.selectedCluster = selectedCluster;
}
@@ -66,11 +66,11 @@ export class LanguageServiceConfig implements ProducerLaunchStateProvider, Consu
return topics.find(topic => topic.id === topicId);
}
-
- public setAutoCreateConfig(autoCreateConfig : BrokerConfigs.AutoCreateTopicResult) {
- this.autoCreateConfig= autoCreateConfig;
+
+ public setAutoCreateConfig(autoCreateConfig: BrokerConfigs.AutoCreateTopicResult) {
+ this.autoCreateConfig = autoCreateConfig;
}
-
+
async getAutoCreateTopicEnabled(clusterid: string): Promise {
return this.autoCreateConfig;
}
@@ -159,6 +159,29 @@ export async function assertDiagnostics(content: string, expected: Arrayactual.contents[0]).value, (expected.contents[0]).value);
+ }
+}
+
// Kafka parser assert
export interface ExpectedChunckResult {