Skip to content
Merged
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
23 changes: 23 additions & 0 deletions javascript/packages/language-server/src/action_view_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export interface ActionViewHelperInfo {
signature: string
documentationURL: string
}

export const ACTION_VIEW_HELPERS: Record<string, ActionViewHelperInfo> = {
"ActionView::Helpers::TagHelper#tag": {
signature: "tag.<tag name>(optional content, options)",
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag",
},
"ActionView::Helpers::TagHelper#content_tag": {
signature: "content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)",
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag",
},
"ActionView::Helpers::UrlHelper#link_to": {
signature: "link_to(name = nil, options = nil, html_options = nil, &block)",
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to",
},
"Turbo::FramesHelper#turbo_frame_tag": {
signature: "turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)",
documentationURL: "https://www.rubydoc.info/github/hotwired/turbo-rails/Turbo/FramesHelper:turbo_frame_tag",
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Visitor } from "@herb-tools/core"
import { ParserService } from "./parser_service"

import { isERBIfNode, isERBElseNode, isHTMLOpenTagNode } from "@herb-tools/core"
import { erbTagToRange, tokenToRange, nodeToRange, openTagRanges } from "./range_utils"
import { erbTagToRange, tokenToRange, nodeToRange, openTagRanges, isPositionInRange, rangeSize } from "./range_utils"

import type {
Node,
Expand Down Expand Up @@ -245,10 +245,10 @@ export class DocumentHighlightService {
let bestSize = Infinity

for (const group of collector.groups) {
const matchingRange = group.find(range => this.isPositionInRange(position, range))
const matchingRange = group.find(range => isPositionInRange(position, range))

if (matchingRange) {
const size = this.rangeSize(matchingRange)
const size = rangeSize(matchingRange)

if (size < bestSize) {
bestSize = size
Expand All @@ -264,27 +264,4 @@ export class DocumentHighlightService {
return []
}

private rangeSize(range: Range): number {
if (range.start.line === range.end.line) {
return range.end.character - range.start.character
}

return (range.end.line - range.start.line) * 10000 + range.end.character
}

private isPositionInRange(position: Position, range: Range): boolean {
if (position.line < range.start.line || position.line > range.end.line) {
return false
}

if (position.line === range.start.line && position.character < range.start.character) {
return false
}

if (position.line === range.end.line && position.character > range.end.character) {
return false
}

return true
}
}
90 changes: 90 additions & 0 deletions javascript/packages/language-server/src/hover_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Hover, MarkupKind, Position } from "vscode-languageserver/node"
import { TextDocument } from "vscode-languageserver-textdocument"

import { Visitor } from "@herb-tools/node-wasm"
import { IdentityPrinter } from "@herb-tools/printer"
import { ActionViewTagHelperToHTMLRewriter } from "@herb-tools/rewriter"
import { isERBOpenTagNode } from "@herb-tools/core"
import { ParserService } from "./parser_service"
import { nodeToRange, isPositionInRange, rangeSize } from "./range_utils"
import { ACTION_VIEW_HELPERS } from "./action_view_helpers"

import type { Node, HTMLElementNode } from "@herb-tools/core"
import type { Range } from "vscode-languageserver/node"

class ActionViewElementCollector extends Visitor {
public elements: { node: HTMLElementNode; range: Range }[] = []

visitHTMLElementNode(node: HTMLElementNode): void {
if (node.element_source && node.element_source !== "HTML" && isERBOpenTagNode(node.open_tag)) {
this.elements.push({
node,
range: nodeToRange(node),
})
}

this.visitChildNodes(node)
}
}

export class HoverService {
private parserService: ParserService

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

getHover(textDocument: TextDocument, position: Position): Hover | null {
const parseResult = this.parserService.parseContent(textDocument.getText(), {
action_view_helpers: true,
track_whitespace: true,
})

const collector = new ActionViewElementCollector()
collector.visit(parseResult.value)

let bestElement: { node: HTMLElementNode; range: Range } | null = null
let bestSize = Infinity

for (const element of collector.elements) {
if (isPositionInRange(position, element.range)) {
const size = rangeSize(element.range)

if (size < bestSize) {
bestSize = size
bestElement = element
}
}
}

if (!bestElement) {
return null
}

const elementSource = bestElement.node.element_source
const rewriter = new ActionViewTagHelperToHTMLRewriter()
const rewrittenNode = rewriter.rewrite(bestElement.node as Node, { baseDir: process.cwd() })
const htmlOutput = IdentityPrinter.print(rewrittenNode)
const helper = ACTION_VIEW_HELPERS[elementSource]
const parts: string[] = []

if (helper) {
parts.push(`\`\`\`ruby\n${helper.signature}\n\`\`\``)
}

parts.push(`**HTML equivalent**\n\`\`\`erb\n${htmlOutput.trim()}\n\`\`\``)

if (helper) {
parts.push(`[${elementSource}](${helper.documentationURL})`)
}

return {
contents: {
kind: MarkupKind.Markdown,
value: parts.join("\n\n"),
},
range: bestElement.range,
}
}

}
4 changes: 2 additions & 2 deletions javascript/packages/language-server/src/parser_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver/node"
import { TextDocument } from "vscode-languageserver-textdocument"
import { Herb, Visitor } from "@herb-tools/node-wasm"

import type { Node, HerbError, DocumentNode, ParseResult } from "@herb-tools/node-wasm"
import type { Node, HerbError, DocumentNode, ParseResult, ParseOptions } from "@herb-tools/node-wasm"

import { lspRangeFromLocation } from "./range_utils"

Expand Down Expand Up @@ -52,7 +52,7 @@ export class ParserService {
}
}

parseContent(content: string, options?: { track_whitespace?: boolean }): ParseResult {
parseContent(content: string, options?: ParseOptions): ParseResult {
return Herb.parse(content, options)
}
}
24 changes: 24 additions & 0 deletions javascript/packages/language-server/src/range_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ export function openTagRanges(tag: HTMLOpenTagNode): (Range | null)[] {
return ranges
}

export function isPositionInRange(position: Position, range: Range): boolean {
if (position.line < range.start.line || position.line > range.end.line) {
return false
}

if (position.line === range.start.line && position.character < range.start.character) {
return false
}

if (position.line === range.end.line && position.character > range.end.character) {
return false
}

return true
}

export function rangeSize(range: Range): number {
if (range.start.line === range.end.line) {
return range.end.character - range.start.character
}

return (range.end.line - range.start.line) * 10000 + range.end.character
}

/**
* Returns a Range that spans the entire document
*/
Expand Down
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,
HoverParams,
TextDocumentIdentifier,
Range,
} from "vscode-languageserver/node"
Expand Down Expand Up @@ -59,6 +60,7 @@ export class Server {
},
foldingRangeProvider: true,
documentHighlightProvider: true,
hoverProvider: true,
},
}

Expand Down Expand Up @@ -171,6 +173,14 @@ export class Server {
return this.service.documentHighlightService.getDocumentHighlights(document, params.position)
})

this.connection.onHover((params: HoverParams) => {
const document = this.service.documentService.get(params.textDocument.uri)

if (!document) return null

return this.service.hoverService.getHover(document, params.position)
})

this.connection.onCodeAction((params: CodeActionParams) => {
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 { HoverService } from "./hover_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
hoverService: HoverService
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.hoverService = new HoverService(this.parserService)
this.commentService = new CommentService(this.parserService)

if (params.initializationOptions) {
Expand Down
Loading
Loading