Skip to content
Open
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
105 changes: 105 additions & 0 deletions javascript/packages/language-server/src/comment_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export class CommentService {
continue
}

if (this.lineIsInsideMultilineERBTag(document, line)) {
const context = lineText.trimStart().startsWith("#") ? "erb-comment" : "erb-tag"

lineInfos.push({ line, context, node: null })
continue
}

const htmlCommentNode = collector.htmlCommentNodesPerLine.get(line)
const info = collector.lineMap.get(line)

Expand Down Expand Up @@ -89,6 +96,10 @@ export class CommentService {
const startLine = range.start.line
const endLine = range.end.line

if (this.selectionIsInsideMultilineERBTag(document, startLine, endLine)) {
return this.toggleRubyLineComments(document, startLine, endLine)
}

const firstLineText = document.getText(Range.create(startLine, 0, startLine + 1, 0)).replace(/\n$/, "")
const lastLineText = document.getText(Range.create(endLine, 0, endLine + 1, 0)).replace(/\n$/, "")
const isWrapped = firstLineText.trim() === "<% if false %>" && lastLineText.trim() === "<% end %>"
Expand Down Expand Up @@ -127,6 +138,10 @@ export class CommentService {
return TextEdit.insert(Position.create(info.line, insertColumn), "#")
}

if (info.context === "erb-tag" && erbNodes.length === 0) {
return TextEdit.replace(lineRange, `${indent}# ${content}`)
}

const result = commentLineContent(content, erbNodes, strategy, this.parserService)

return TextEdit.replace(lineRange, indent + result)
Expand All @@ -145,6 +160,77 @@ export class CommentService {
return null
}

private lineIsInsideMultilineERBTag(document: TextDocument, targetLine: number): boolean {
let insideERB = false

for (let line = 0; line <= targetLine; line++) {
const lineText = document.getText(Range.create(line, 0, line + 1, 0)).replace(/\n$/, "")
const insideAtStartOfLine = insideERB
let index = 0

while (index < lineText.length) {
const erbStart = lineText.indexOf("<%", index)
const erbEnd = lineText.indexOf("%>", index)

if (erbEnd !== -1 && (erbStart === -1 || erbEnd < erbStart)) {
insideERB = false
index = erbEnd + 2
} else if (erbStart !== -1) {
insideERB = true
index = erbStart + 2
} else {
break
}
}

if (line === targetLine) {
return insideAtStartOfLine && !lineText.trimStart().startsWith("%>")
}
}

return false
}

private selectionIsInsideMultilineERBTag(document: TextDocument, startLine: number, endLine: number): boolean {
let hasRubyLine = false

for (let line = startLine; line <= endLine; line++) {
const lineText = document.getText(Range.create(line, 0, line + 1, 0)).replace(/\n$/, "")

if (lineText.trim() === "") continue
if (!this.lineIsInsideMultilineERBTag(document, line)) return false

hasRubyLine = true
}

return hasRubyLine
}

private toggleRubyLineComments(document: TextDocument, startLine: number, endLine: number): TextEdit[] {
const lineTexts: { line: number, text: string }[] = []

for (let line = startLine; line <= endLine; line++) {
const text = document.getText(Range.create(line, 0, line + 1, 0)).replace(/\n$/, "")

if (text.trim() !== "") {
lineTexts.push({ line, text })
}
}

const allCommented = lineTexts.every(({ text }) => text.trimStart().startsWith("#"))

return lineTexts.map(({ line, text }) => {
if (allCommented) {
return this.uncommentRubyLine(text, line)!
}

const indent = this.getIndentation(text)
const content = text.trimStart()

return TextEdit.replace(Range.create(line, 0, line, text.length), `${indent}# ${content}`)
}).filter(edit => edit !== null)
}

private uncommentLine(info: LineInfo, lineText: string, collector: LineContextCollector): TextEdit | null {
const lineRange = Range.create(info.line, 0, info.line, lineText.length)
const indent = this.getIndentation(lineText)
Expand All @@ -155,6 +241,10 @@ export class CommentService {
}

if (info.context === "erb-comment") {
if (!info.node) {
return this.uncommentRubyLine(lineText, info.line)
}

const node = info.node as ERBContentNode
if (!node?.tag_opening || !node?.tag_closing) return null

Expand Down Expand Up @@ -192,6 +282,10 @@ export class CommentService {
return TextEdit.del(Range.create(info.line, hashColumn, info.line, hashColumn + 1))
}

if (info.context === "erb-tag") {
return this.uncommentRubyLine(lineText, info.line)
}

if (info.context === "html-comment") {
const commentNode = info.node as HTMLCommentNode | null

Expand All @@ -209,6 +303,17 @@ export class CommentService {
return null
}

private uncommentRubyLine(lineText: string, line: number): TextEdit | null {
const content = lineText.trimStart()

if (!content.startsWith("#")) return null

const hashColumn = lineText.length - content.length
const deleteLength = content.startsWith("# ") ? 2 : 1

return TextEdit.del(Range.create(line, hashColumn, line, hashColumn + deleteLength))
}

private htmlCommentSpansLine(node: HTMLCommentNode, lineText: string): boolean {
if (!node.comment_start || !node.comment_end) return false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class LineContextCollector extends Visitor {
if (!node.tag_opening || !node.tag_closing) return

const startLine = lspLine(node.tag_opening.location.start)
const endLine = lspLine(node.tag_closing.location.end)

const nodes = this.erbNodesPerLine.get(startLine) || []
nodes.push(node as ERBContentNode)
Expand All @@ -30,7 +31,9 @@ export class LineContextCollector extends Visitor {
if (isERBCommentNode(node)) {
this.setLine(startLine, "erb-comment", node)
} else {
this.setLine(startLine, "erb-tag", node)
for (let line = startLine; line <= endLine; line++) {
this.setLine(line, "erb-tag", node)
}
}
}

Expand Down
64 changes: 64 additions & 0 deletions javascript/packages/language-server/test/comment_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,22 @@ describe("CommentService", () => {
expect(applyEdits(original, edits)).toBe(`<%# a %><%# b %><%# c %>`)
})

it("comments an internal Ruby line inside a multiline ERB tag", () => {
const original = `<%\n a\n%>`
const document = createDocument(original)
const edits = service.toggleLineComment(document, lineRange(1))

expect(applyEdits(original, edits)).toBe(`<%\n # a\n%>`)
})

it("comments a multiline ERB tag selection including delimiters", () => {
const original = `<%\n a\n%>`
const document = createDocument(original)
const edits = service.toggleLineComment(document, lineRange(0, 2))

expect(applyEdits(original, edits)).toBe(`<%#\n # a\n# %>`)
})

it("comments multiple adjacent ERB output tags as all-erb", () => {
const original = `<%= a %><%= b %><%= c %>`
const document = createDocument(original)
Expand Down Expand Up @@ -992,6 +1008,33 @@ describe("CommentService", () => {
expect(uncommented).toBe(original)
})

it("round-trips an internal Ruby line inside a multiline ERB tag", () => {
const original = `<%\n a\n%>`
const document1 = createDocument(original)
const commented = applyEdits(original, service.toggleLineComment(document1, lineRange(1)))

expect(commented).toBe(`<%\n # a\n%>`)

const document2 = createDocument(commented)
const uncommented = applyEdits(commented, service.toggleLineComment(document2, lineRange(1)))

expect(uncommented).toBe(original)
})

it("round-trips a multiline ERB tag selection including delimiters", () => {
const original = `<%\n a\n%>`
const range = lineRange(0, 2)
const document1 = createDocument(original)
const commented = applyEdits(original, service.toggleLineComment(document1, range))

expect(commented).toBe(`<%#\n # a\n# %>`)

const document2 = createDocument(commented)
const uncommented = applyEdits(commented, service.toggleLineComment(document2, range))

expect(uncommented).toBe(original)
})

it("round-trips a real-world ERB template", () => {
const original = dedent`
<% @records.each do |record| %>
Expand Down Expand Up @@ -1065,6 +1108,14 @@ describe("CommentService", () => {
const startEdit = edits.find(edit => edit.newText.includes("<% if false %>"))
expect(startEdit?.newText).toBe(" <% if false %>\n")
})

it("comments an internal Ruby line inside a multiline ERB tag", () => {
const original = `<%\n a\n%>`
const document = createDocument(original)
const edits = service.toggleBlockComment(document, lineRange(1))

expect(applyEdits(original, edits)).toBe(`<%\n # a\n%>`)
})
})

describe("round-trip", () => {
Expand All @@ -1089,6 +1140,19 @@ describe("CommentService", () => {
<%= render "thing" %>
`.trim())
})

it("round-trips an internal Ruby line inside a multiline ERB tag", () => {
const original = `<%\n a\n%>`
const document1 = createDocument(original)
const commented = applyEdits(original, service.toggleBlockComment(document1, lineRange(1)))

expect(commented).toBe(`<%\n # a\n%>`)

const document2 = createDocument(commented)
const uncommented = applyEdits(commented, service.toggleBlockComment(document2, lineRange(1)))

expect(uncommented).toBe(original)
})
})

describe("unwrapping", () => {
Expand Down
Loading