Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions javascript/packages/language-server/src/inlay_hint_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { InlayHint, InlayHintKind } from "vscode-languageserver/node"
import { TextDocument } from "vscode-languageserver-textdocument"

import { Visitor, isHTMLOpenTagNode, isLiteralNode } from "@herb-tools/core"
import { ParserService } from "./parser_service"
import { lspPosition } from "./range_utils"

import type {
ERBEndNode,
ERBIfNode,
ERBUnlessNode,
ERBBlockNode,
ERBCaseNode,
ERBCaseMatchNode,
ERBWhileNode,
ERBUntilNode,
ERBForNode,
ERBBeginNode,
HTMLElementNode,
HTMLOpenTagNode,
HTMLAttributeNode,
Node,
} from "@herb-tools/core"

type ERBNodeWithEnd = ERBIfNode | ERBUnlessNode | ERBBlockNode | ERBCaseNode | ERBCaseMatchNode | ERBWhileNode | ERBUntilNode | ERBForNode | ERBBeginNode

function labelForERBNode(node: ERBNodeWithEnd): string | null {
const content = node.content?.value?.trim()

if (!content) return null

return `# ${content}`
}

function findAttributeValue(openTag: HTMLOpenTagNode, attributeName: string): string | null {
for (const child of openTag.children) {
if (child.type !== "AST_HTML_ATTRIBUTE_NODE") continue

const attrNode = child as unknown as HTMLAttributeNode
if (!attrNode.name || !attrNode.value) continue

const nameStr = attrNode.name.children
.filter(isLiteralNode)
.map(n => n.content)
.join("")

if (nameStr !== attributeName) continue

const valueStr = attrNode.value.children
.filter(isLiteralNode)
.map(n => n.content)
.join("")

return valueStr
}

return null
}

function labelForHTMLElement(node: HTMLElementNode): string | null {
if (!node.open_tag || !isHTMLOpenTagNode(node.open_tag)) return null

const id = findAttributeValue(node.open_tag, "id")
if (id) return `<!-- #${id} -->`

const className = findAttributeValue(node.open_tag, "class")
if (className) return `<!-- .${className.split(/\s+/).join(".")} -->`

return null
}

export class InlayHintCollector extends Visitor {
public hints: InlayHint[] = []

private addERBEndNodeHint(node: ERBNodeWithEnd): void {
const endNode: ERBEndNode | null = node.end_node
if (!endNode?.tag_closing) return

const label = labelForERBNode(node)
if (!label) return

const endLine = endNode.location.start.line
const nodeLine = node.location.start.line

if (endLine - nodeLine < 2) return

this.hints.push({
position: lspPosition(endNode.tag_closing.location.end),
label: ` ${label}`,
kind: InlayHintKind.Parameter,
paddingLeft: true,
})
}

visitHTMLElementNode(node: HTMLElementNode): void {
if (node.close_tag && node.open_tag) {
const endLine = node.close_tag.location.start.line
const startLine = node.open_tag.location.start.line

if (endLine - startLine >= 2) {
const label = labelForHTMLElement(node)

if (label) {
this.hints.push({
position: lspPosition(node.close_tag.location.end),
label: ` ${label}`,
kind: InlayHintKind.Parameter,
paddingLeft: true,
})
}
}
}

this.visitChildNodes(node)
}

visitERBIfNode(node: ERBIfNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBUnlessNode(node: ERBUnlessNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBBlockNode(node: ERBBlockNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBCaseNode(node: ERBCaseNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBWhileNode(node: ERBWhileNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBUntilNode(node: ERBUntilNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBForNode(node: ERBForNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}

visitERBBeginNode(node: ERBBeginNode): void {
this.addERBEndNodeHint(node)
this.visitChildNodes(node)
}
}

export class InlayHintService {
private parserService: ParserService

constructor(parserService: ParserService) {
this.parserService = parserService
}

getInlayHints(textDocument: TextDocument): InlayHint[] {
const parseResult = this.parserService.parseDocument(textDocument)
const collector = new InlayHintCollector()

collector.visit(parseResult.document)

return collector.hints
}
}
10 changes: 10 additions & 0 deletions javascript/packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CodeActionKind,
FoldingRangeParams,
DocumentHighlightParams,
InlayHintParams,
TextDocumentIdentifier,
Range,
} from "vscode-languageserver/node"
Expand Down Expand Up @@ -59,6 +60,7 @@ export class Server {
},
foldingRangeProvider: true,
documentHighlightProvider: true,
inlayHintProvider: true,
},
}

Expand Down Expand Up @@ -198,6 +200,14 @@ export class Server {
return this.service.foldingRangeService.getFoldingRanges(document)
})

this.connection.languages.inlayHint.on((params: InlayHintParams) => {
const document = this.service.documentService.get(params.textDocument.uri)

if (!document) return []

return this.service.inlayHintService.getInlayHints(document)
})

this.connection.onRequest('herb/toggleLineComment', (params: { textDocument: TextDocumentIdentifier, range: Range }) => {
const document = this.service.documentService.get(params.textDocument.uri)

Expand Down
3 changes: 3 additions & 0 deletions javascript/packages/language-server/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CodeActionService } from "./code_action_service"
import { DocumentSaveService } from "./document_save_service"
import { FoldingRangeService } from "./folding_range_service"
import { DocumentHighlightService } from "./document_highlight_service"
import { InlayHintService } from "./inlay_hint_service"
import { CommentService } from "./comment_service"

import { version } from "../package.json"
Expand All @@ -35,6 +36,7 @@ export class Service {
documentSaveService: DocumentSaveService
foldingRangeService: FoldingRangeService
documentHighlightService: DocumentHighlightService
inlayHintService: InlayHintService
commentService: CommentService

constructor(connection: Connection, params: InitializeParams) {
Expand All @@ -52,6 +54,7 @@ export class Service {
this.documentSaveService = new DocumentSaveService(this.connection, this.settings, this.autofixService, this.formattingService)
this.foldingRangeService = new FoldingRangeService(this.parserService)
this.documentHighlightService = new DocumentHighlightService(this.parserService)
this.inlayHintService = new InlayHintService(this.parserService)
this.commentService = new CommentService(this.parserService)

if (params.initializationOptions) {
Expand Down
Loading
Loading