Skip to content

Commit

Permalink
Provide documentation on hover, in .kafka files
Browse files Browse the repository at this point in the history
Fixes jlandersen#149

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Apr 26, 2021
1 parent 7191910 commit ae53c98
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 18 deletions.
18 changes: 17 additions & 1 deletion src/kafka-file/kafkaFileClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ export function startLanguageClient(
const diagnostics = new KafkaFileDiagnostics(kafkaFileDocuments, languageService, 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') {
Expand Down Expand Up @@ -184,7 +190,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);
}

}
Expand Down Expand Up @@ -251,3 +257,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<vscode.Hover> {
return runSafeAsync(async () => {
const kafkaFileDocument = this.getKafkaFileDocument(document);
return this.languageService.doHover(document, kafkaFileDocument, position);
}, null, `Error while computing hover for ${document.uri}`, token);
}

}
23 changes: 21 additions & 2 deletions src/kafka-file/languageservice/kafkaFileLanguageService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { CodeLens, CompletionList, Diagnostic, Position, TextDocument, Uri } from "vscode";
import { CodeLens, CompletionList, Diagnostic, Hover, Position, TextDocument, Uri } from "vscode";
import { ConsumerLaunchState } from "../../client";
import { ProducerLaunchState } from "../../client/producer";
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.
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface TopicProvider {
*
*/
export interface LanguageService {

/**
* Parse the given text document and returns an AST.
*
Expand Down Expand Up @@ -81,6 +83,15 @@ export interface LanguageService {
* @param kafkaFileDocument the parsed AST.
*/
doDiagnostics(document: TextDocument, kafkaFileDocument: KafkaFileDocument, producerFakerJSEnabled : boolean): Diagnostic[];

/**
* 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<Hover | undefined>;
}

/**
Expand All @@ -96,10 +107,18 @@ export function getLanguageService(producerLaunchStateProvider: ProducerLaunchSt
const codeLenses = new KafkaFileCodeLenses(producerLaunchStateProvider, consumerLaunchStateProvider, selectedClusterProvider);
const completion = new KafkaFileCompletion(selectedClusterProvider, topicProvider);
const diagnostics = new KafkaFileDiagnostics();
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`;
}
37 changes: 31 additions & 6 deletions src/kafka-file/languageservice/parser/kafkaFileParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; }
}

Expand Down Expand Up @@ -65,6 +77,17 @@ class ChildrenNode<T extends Node> 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; };
}

Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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<Property | Chunk> {
Expand Down
9 changes: 2 additions & 7 deletions src/kafka-file/languageservice/services/completion.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -235,19 +235,14 @@ 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);
topics.forEach((topic) => {
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;
Expand Down
4 changes: 2 additions & 2 deletions src/kafka-file/languageservice/services/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,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;
}
Expand Down
170 changes: 170 additions & 0 deletions src/kafka-file/languageservice/services/hover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { Hover, Position, Range, TextDocument } from "vscode";
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<Hover | undefined> {
// 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 = <Block>node;
const topic = block.getPropertyValue('topic');
return new Hover(
'Consumer declaration for the topic `' + topic +
'`. See [here](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Consuming.md#kafka-file) for more informations.',
node.range());
}

case NodeKind.producerBlock: {
const block = <Block>node;
const topic = block.getPropertyValue('topic');
return new Hover(
'Producer declaration for the topic `' + topic +
'`. See [here](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#kafka-file) for more informations.',
node.range());
}

case NodeKind.propertyKey: {
const propertyKey = <Chunk>node;
const property = <Property>propertyKey.parent;
const propertyName = propertyKey.content;
const propertyKeyRange = propertyKey.range();
const block = <Block>property.parent;
if (block.type === BlockType.consumer) {
// CONSUMER
// key|:

// or

// CONSUMER
// key|
return await this.getHoverForConsumerPropertyNames(propertyName, propertyKeyRange, <ConsumerBlock>block);
} else {
// PRODUCER
// key|:
return await this.getHoverForProducerPropertyNames(propertyName, propertyKeyRange, <ProducerBlock>block);
}
}

case NodeKind.propertyValue: {
const propertyValue = <Chunk>node;
const property = <Property>propertyValue.parent;
const block = <Block>property.parent;
if (block.type === BlockType.consumer) {
// CONSUMER
// key-format: |
return await this.getHoverForConsumerPropertyValues(propertyValue, property, <ConsumerBlock>block);
} else {
// PRODUCER
// key-format: |
return await this.getHoverForProducerPropertyValues(propertyValue, property, <ProducerBlock>block);
}
}

case NodeKind.mustacheExpression: {
const expression = <MustacheExpression>node;
return new Hover('FakerJS expression. See [here](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#randomized-content) for more informations.', expression.enclosedExpressionRange);
}

case NodeKind.producerValue: {
return new Hover('Producer value. See [here](https://github.com/jlandersen/vscode-kafka/blob/master/docs/Producing.md#kafka-file) for more informations.', node.range());
}
}
}

async getHoverForConsumerPropertyNames(propertyName: string, propertyKeyRange: Range, block: ConsumerBlock): Promise<Hover | undefined> {
return await this.getHoverForPropertyNames(propertyName, propertyKeyRange, block, consumerModel);
}

async getHoverForProducerPropertyNames(propertyName: string, propertyKeyRange: Range, block: ProducerBlock): Promise<Hover | undefined> {
return await this.getHoverForPropertyNames(propertyName, propertyKeyRange, block, producerModel);
}

async getHoverForPropertyNames(propertyName: string, propertyKeyRange: Range, block: Block, metadata: Model): Promise<Hover | undefined> {
const definition = metadata.getDefinition(propertyName);
if (definition && definition.description) {
return new Hover(definition.description, propertyKeyRange);
}
}

async getHoverForConsumerPropertyValues(propertyValue: any, property: Property, block: ConsumerBlock): Promise<Hover | undefined> {
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: any, property: Property, block: ProducerBlock): Promise<Hover | undefined> {
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<Hover | undefined> {
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 new Hover(createTopicDocumentation(topic), propertyValue.range());
}
}
}
catch (e) {
return;
}

return undefined;
}

async getHoverForPropertyValues(propertyValue: Chunk | undefined, property: Property, block: Block, metadata: Model): Promise<Hover | undefined> {
const propertyName = property.propertyName;
if (!propertyName) {
return;
}
const definition = metadata.getDefinition(propertyName);
if (definition && !definition.description) {
return new Hover(definition.description, propertyValue?.range());
}
return undefined;
}

}

0 comments on commit ae53c98

Please sign in to comment.