@herb-tools/language-server 0.8.10 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action_view_helpers.js +19 -0
- package/dist/action_view_helpers.js.map +1 -0
- package/dist/autofix_service.js +1 -1
- package/dist/autofix_service.js.map +1 -1
- package/dist/code_action_service.js +3 -6
- package/dist/code_action_service.js.map +1 -1
- package/dist/comment_ast_utils.js +206 -0
- package/dist/comment_ast_utils.js.map +1 -0
- package/dist/comment_service.js +175 -0
- package/dist/comment_service.js.map +1 -0
- package/dist/diagnostics.js +15 -27
- package/dist/diagnostics.js.map +1 -1
- package/dist/document_highlight_service.js +196 -0
- package/dist/document_highlight_service.js.map +1 -0
- package/dist/document_save_service.js +16 -6
- package/dist/document_save_service.js.map +1 -1
- package/dist/folding_range_service.js +209 -0
- package/dist/folding_range_service.js.map +1 -0
- package/dist/formatting_service.js +4 -4
- package/dist/formatting_service.js.map +1 -1
- package/dist/herb-language-server.js +150189 -41260
- package/dist/herb-language-server.js.map +1 -1
- package/dist/hover_service.js +70 -0
- package/dist/hover_service.js.map +1 -0
- package/dist/index.cjs +1227 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/line_context_collector.js +73 -0
- package/dist/line_context_collector.js.map +1 -0
- package/dist/linter_service.js +20 -4
- package/dist/linter_service.js.map +1 -1
- package/dist/parser_service.js +6 -5
- package/dist/parser_service.js.map +1 -1
- package/dist/range_utils.js +65 -0
- package/dist/range_utils.js.map +1 -0
- package/dist/rewrite_code_action_service.js +135 -0
- package/dist/rewrite_code_action_service.js.map +1 -0
- package/dist/server.js +36 -2
- package/dist/server.js.map +1 -1
- package/dist/service.js +10 -0
- package/dist/service.js.map +1 -1
- package/dist/types/action_view_helpers.d.ts +5 -0
- package/dist/types/comment_ast_utils.d.ts +20 -0
- package/dist/types/comment_service.d.ts +14 -0
- package/dist/types/diagnostics.d.ts +0 -1
- package/dist/types/document_highlight_service.d.ts +28 -0
- package/dist/types/document_save_service.d.ts +8 -0
- package/dist/types/folding_range_service.d.ts +35 -0
- package/dist/types/formatting_service.d.ts +1 -1
- package/dist/types/hover_service.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/line_context_collector.d.ts +19 -0
- package/dist/types/linter_service.d.ts +1 -0
- package/dist/types/parser_service.d.ts +2 -1
- package/dist/types/range_utils.d.ts +16 -0
- package/dist/types/rewrite_code_action_service.d.ts +11 -0
- package/dist/types/service.d.ts +10 -0
- package/dist/types/utils.d.ts +0 -6
- package/dist/utils.js +0 -16
- package/dist/utils.js.map +1 -1
- package/package.json +10 -5
- package/src/action_view_helpers.ts +23 -0
- package/src/autofix_service.ts +1 -1
- package/src/code_action_service.ts +3 -6
- package/src/comment_ast_utils.ts +282 -0
- package/src/comment_service.ts +228 -0
- package/src/diagnostics.ts +24 -38
- package/src/document_highlight_service.ts +267 -0
- package/src/document_save_service.ts +19 -7
- package/src/folding_range_service.ts +287 -0
- package/src/formatting_service.ts +4 -4
- package/src/hover_service.ts +90 -0
- package/src/index.ts +4 -0
- package/src/line_context_collector.ts +97 -0
- package/src/linter_service.ts +25 -7
- package/src/parser_service.ts +9 -10
- package/src/range_utils.ts +90 -0
- package/src/rewrite_code_action_service.ts +165 -0
- package/src/server.ts +51 -2
- package/src/service.ts +15 -0
- package/src/utils.ts +0 -22
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CodeAction, Range } from "vscode-languageserver/node";
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
import { ParserService } from "./parser_service";
|
|
4
|
+
export declare class RewriteCodeActionService {
|
|
5
|
+
private parserService;
|
|
6
|
+
constructor(parserService: ParserService);
|
|
7
|
+
getCodeActions(document: TextDocument, requestedRange: Range): CodeAction[];
|
|
8
|
+
private createActionViewToHTMLAction;
|
|
9
|
+
private createHTMLToActionViewAction;
|
|
10
|
+
private rangesOverlap;
|
|
11
|
+
}
|
package/dist/types/service.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ import { ConfigService } from "./config_service";
|
|
|
11
11
|
import { AutofixService } from "./autofix_service";
|
|
12
12
|
import { CodeActionService } from "./code_action_service";
|
|
13
13
|
import { DocumentSaveService } from "./document_save_service";
|
|
14
|
+
import { FoldingRangeService } from "./folding_range_service";
|
|
15
|
+
import { DocumentHighlightService } from "./document_highlight_service";
|
|
16
|
+
import { HoverService } from "./hover_service";
|
|
17
|
+
import { RewriteCodeActionService } from "./rewrite_code_action_service";
|
|
18
|
+
import { CommentService } from "./comment_service";
|
|
14
19
|
export declare class Service {
|
|
15
20
|
connection: Connection;
|
|
16
21
|
settings: Settings;
|
|
@@ -25,6 +30,11 @@ export declare class Service {
|
|
|
25
30
|
configService: ConfigService;
|
|
26
31
|
codeActionService: CodeActionService;
|
|
27
32
|
documentSaveService: DocumentSaveService;
|
|
33
|
+
foldingRangeService: FoldingRangeService;
|
|
34
|
+
documentHighlightService: DocumentHighlightService;
|
|
35
|
+
hoverService: HoverService;
|
|
36
|
+
rewriteCodeActionService: RewriteCodeActionService;
|
|
37
|
+
commentService: CommentService;
|
|
28
38
|
constructor(connection: Connection, params: InitializeParams);
|
|
29
39
|
init(): Promise<void>;
|
|
30
40
|
refresh(): Promise<void>;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from "vscode-languageserver/node";
|
|
2
2
|
import type { LintSeverity } from "@herb-tools/linter";
|
|
3
|
-
import type { Range } from "vscode-languageserver/node";
|
|
4
|
-
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
5
3
|
export declare function camelize(value: string): string;
|
|
6
4
|
export declare function dasherize(value: string): string;
|
|
7
5
|
export declare function capitalize(value: string): string;
|
|
8
6
|
export declare function lintToDignosticSeverity(severity: LintSeverity): DiagnosticSeverity;
|
|
9
|
-
/**
|
|
10
|
-
* Returns a Range that spans the entire document
|
|
11
|
-
*/
|
|
12
|
-
export declare function getFullDocumentRange(document: TextDocument): Range;
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from "vscode-languageserver/node";
|
|
2
|
-
import { Position } from "vscode-languageserver/node";
|
|
3
2
|
export function camelize(value) {
|
|
4
3
|
return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
5
4
|
}
|
|
@@ -17,19 +16,4 @@ export function lintToDignosticSeverity(severity) {
|
|
|
17
16
|
case "hint": return DiagnosticSeverity.Hint;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Returns a Range that spans the entire document
|
|
22
|
-
*/
|
|
23
|
-
export function getFullDocumentRange(document) {
|
|
24
|
-
const lastLine = document.lineCount - 1;
|
|
25
|
-
const lastLineText = document.getText({
|
|
26
|
-
start: Position.create(lastLine, 0),
|
|
27
|
-
end: Position.create(lastLine + 1, 0)
|
|
28
|
-
});
|
|
29
|
-
const lastLineLength = lastLineText.length;
|
|
30
|
-
return {
|
|
31
|
-
start: Position.create(0, 0),
|
|
32
|
-
end: Position.create(lastLine, lastLineLength)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
19
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAG/D,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AACzE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACvD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAsB;IAC5D,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,OAAO,CAAC,CAAC,OAAO,kBAAkB,CAAC,KAAK,CAAA;QAC7C,KAAK,SAAS,CAAC,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAA;QACjD,KAAK,MAAM,CAAC,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAA;QAClD,KAAK,MAAM,CAAC,CAAC,OAAO,kBAAkB,CAAC,IAAI,CAAA;IAC7C,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/language-server",
|
|
3
3
|
"description": "Herb HTML+ERB Language Tools and Language Server Protocol integration.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"author": "Marco Roth",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
@@ -45,12 +45,17 @@
|
|
|
45
45
|
"dist/"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@herb-tools/config": "0.
|
|
49
|
-
"@herb-tools/formatter": "0.
|
|
50
|
-
"@herb-tools/linter": "0.
|
|
51
|
-
"@herb-tools/node-wasm": "0.
|
|
48
|
+
"@herb-tools/config": "0.9.0",
|
|
49
|
+
"@herb-tools/formatter": "0.9.0",
|
|
50
|
+
"@herb-tools/linter": "0.9.0",
|
|
51
|
+
"@herb-tools/node-wasm": "0.9.0",
|
|
52
|
+
"@herb-tools/printer": "0.9.0",
|
|
53
|
+
"@herb-tools/rewriter": "0.9.0",
|
|
52
54
|
"dedent": "^1.7.0",
|
|
53
55
|
"vscode-languageserver": "^9.0.1",
|
|
54
56
|
"vscode-languageserver-textdocument": "^1.0.12"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.3.3"
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ActionViewHelperInfo {
|
|
2
|
+
signature: string
|
|
3
|
+
documentationURL: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const ACTION_VIEW_HELPERS: Record<string, ActionViewHelperInfo> = {
|
|
7
|
+
"ActionView::Helpers::TagHelper#tag": {
|
|
8
|
+
signature: "tag.<tag name>(optional content, options)",
|
|
9
|
+
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag",
|
|
10
|
+
},
|
|
11
|
+
"ActionView::Helpers::TagHelper#content_tag": {
|
|
12
|
+
signature: "content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)",
|
|
13
|
+
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag",
|
|
14
|
+
},
|
|
15
|
+
"ActionView::Helpers::UrlHelper#link_to": {
|
|
16
|
+
signature: "link_to(name = nil, options = nil, html_options = nil, &block)",
|
|
17
|
+
documentationURL: "https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to",
|
|
18
|
+
},
|
|
19
|
+
"Turbo::FramesHelper#turbo_frame_tag": {
|
|
20
|
+
signature: "turbo_frame_tag(*ids, src: nil, target: nil, **attributes, &block)",
|
|
21
|
+
documentationURL: "https://www.rubydoc.info/github/hotwired/turbo-rails/Turbo/FramesHelper:turbo_frame_tag",
|
|
22
|
+
},
|
|
23
|
+
}
|
package/src/autofix_service.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Herb } from "@herb-tools/node-wasm"
|
|
|
5
5
|
import { Linter } from "@herb-tools/linter"
|
|
6
6
|
import { Config } from "@herb-tools/config"
|
|
7
7
|
|
|
8
|
-
import { getFullDocumentRange } from "./
|
|
8
|
+
import { getFullDocumentRange } from "./range_utils"
|
|
9
9
|
|
|
10
10
|
export class AutofixService {
|
|
11
11
|
private connection: Connection
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CodeAction, CodeActionKind, CodeActionParams, Diagnostic, Range,
|
|
1
|
+
import { CodeAction, CodeActionKind, CodeActionParams, Diagnostic, Range, TextEdit, WorkspaceEdit, CreateFile, TextDocumentEdit, OptionalVersionedTextDocumentIdentifier } from "vscode-languageserver/node"
|
|
2
2
|
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
3
|
|
|
4
4
|
import { Config } from "@herb-tools/config"
|
|
@@ -6,7 +6,7 @@ import { Project } from "./project"
|
|
|
6
6
|
import { Herb } from "@herb-tools/node-wasm"
|
|
7
7
|
import { Linter } from "@herb-tools/linter"
|
|
8
8
|
|
|
9
|
-
import { getFullDocumentRange } from "./
|
|
9
|
+
import { getFullDocumentRange, lspRangeFromLocation } from "./range_utils"
|
|
10
10
|
|
|
11
11
|
import type { LintOffense } from "@herb-tools/linter"
|
|
12
12
|
|
|
@@ -362,10 +362,7 @@ export class CodeActionService {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
private offenseToRange(offense: LintOffense): Range {
|
|
365
|
-
return
|
|
366
|
-
start: Position.create(offense.location.start.line - 1, offense.location.start.column),
|
|
367
|
-
end: Position.create(offense.location.end.line - 1, offense.location.end.column)
|
|
368
|
-
}
|
|
365
|
+
return lspRangeFromLocation(offense.location)
|
|
369
366
|
}
|
|
370
367
|
|
|
371
368
|
private rangesEqual(r1: Range, r2: Range): boolean {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ParserService } from "./parser_service"
|
|
2
|
+
import { LineContextCollector } from "./line_context_collector"
|
|
3
|
+
|
|
4
|
+
import { HTMLCommentNode, Location, createSyntheticToken } from "@herb-tools/core"
|
|
5
|
+
import { IdentityPrinter } from "@herb-tools/printer"
|
|
6
|
+
|
|
7
|
+
import { isERBCommentNode, isLiteralNode, createLiteral } from "@herb-tools/core"
|
|
8
|
+
import { asMutable } from "@herb-tools/rewriter"
|
|
9
|
+
|
|
10
|
+
import type { Node, ERBContentNode } from "@herb-tools/core"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Commenting strategy for a line:
|
|
14
|
+
* - "single-erb": sole ERB tag on the line → insert # at column offset
|
|
15
|
+
* - "all-erb": multiple ERB tags with no significant HTML → # into each
|
|
16
|
+
* - "per-segment": control-flow ERB wrapping HTML → # each ERB, <!-- --> each HTML segment
|
|
17
|
+
* - "whole-line": output ERB mixed with HTML → wrap entire line in <!-- --> with ERB #
|
|
18
|
+
* - "html-only": pure HTML content → wrap in <!-- -->
|
|
19
|
+
*/
|
|
20
|
+
export type CommentStrategy = "single-erb" | "all-erb" | "per-segment" | "whole-line" | "html-only"
|
|
21
|
+
|
|
22
|
+
interface LineSegment {
|
|
23
|
+
text: string
|
|
24
|
+
isERB: boolean
|
|
25
|
+
node?: ERBContentNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function commentERBNode(node: ERBContentNode): void {
|
|
29
|
+
const mutable = asMutable(node)
|
|
30
|
+
|
|
31
|
+
if (mutable.tag_opening) {
|
|
32
|
+
const currentValue = mutable.tag_opening.value
|
|
33
|
+
|
|
34
|
+
mutable.tag_opening = createSyntheticToken(
|
|
35
|
+
currentValue.substring(0, 2) + "#" + currentValue.substring(2),
|
|
36
|
+
mutable.tag_opening.type
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function uncommentERBNode(node: ERBContentNode): void {
|
|
42
|
+
const mutable = asMutable(node)
|
|
43
|
+
|
|
44
|
+
if (mutable.tag_opening && mutable.tag_opening.value === "<%#") {
|
|
45
|
+
const contentValue = mutable.content?.value || ""
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
contentValue.startsWith(" graphql ") ||
|
|
49
|
+
contentValue.startsWith(" %= ") ||
|
|
50
|
+
contentValue.startsWith(" == ") ||
|
|
51
|
+
contentValue.startsWith(" % ") ||
|
|
52
|
+
contentValue.startsWith(" = ") ||
|
|
53
|
+
contentValue.startsWith(" - ")
|
|
54
|
+
) {
|
|
55
|
+
mutable.tag_opening = createSyntheticToken("<%", mutable.tag_opening.type)
|
|
56
|
+
mutable.content = createSyntheticToken(contentValue.substring(1), mutable.content!.type)
|
|
57
|
+
} else {
|
|
58
|
+
mutable.tag_opening = createSyntheticToken("<%", mutable.tag_opening.type)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function determineStrategy(erbNodes: ERBContentNode[], lineText: string): CommentStrategy {
|
|
64
|
+
if (erbNodes.length === 0) {
|
|
65
|
+
return "html-only"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (erbNodes.length === 1) {
|
|
69
|
+
const node = erbNodes[0]
|
|
70
|
+
if (!node.tag_opening || !node.tag_closing) return "html-only"
|
|
71
|
+
|
|
72
|
+
const nodeStart = node.tag_opening.location.start.column
|
|
73
|
+
const nodeEnd = node.tag_closing.location.end.column
|
|
74
|
+
const isSoleContent = lineText.substring(0, nodeStart).trim() === "" && lineText.substring(nodeEnd).trim() === ""
|
|
75
|
+
|
|
76
|
+
if (isSoleContent) {
|
|
77
|
+
return "single-erb"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return "whole-line"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const segments = getLineSegments(lineText, erbNodes)
|
|
84
|
+
const hasHTML = segments.some(segment => !segment.isERB && segment.text.trim() !== "")
|
|
85
|
+
|
|
86
|
+
if (!hasHTML) {
|
|
87
|
+
return "all-erb"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const allControlTags = erbNodes.every(node => node.tag_opening?.value === "<%")
|
|
91
|
+
|
|
92
|
+
if (allControlTags) {
|
|
93
|
+
return "per-segment"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return "whole-line"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getLineSegments(lineText: string, erbNodes: ERBContentNode[]): LineSegment[] {
|
|
100
|
+
const segments: LineSegment[] = []
|
|
101
|
+
const sorted = [...erbNodes].sort((a, b) => a.tag_opening!.location.start.column - b.tag_opening!.location.start.column)
|
|
102
|
+
|
|
103
|
+
let position = 0
|
|
104
|
+
|
|
105
|
+
for (const node of sorted) {
|
|
106
|
+
const nodeStart = node.tag_opening!.location.start.column
|
|
107
|
+
const nodeEnd = node.tag_closing!.location.end.column
|
|
108
|
+
|
|
109
|
+
if (nodeStart > position) {
|
|
110
|
+
segments.push({ text: lineText.substring(position, nodeStart), isERB: false })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
segments.push({ text: lineText.substring(nodeStart, nodeEnd), isERB: true, node })
|
|
114
|
+
position = nodeEnd
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (position < lineText.length) {
|
|
118
|
+
segments.push({ text: lineText.substring(position), isERB: false })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return segments
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Comment a line using AST mutation for strategies where the parser produces flat children,
|
|
126
|
+
* and text-segment manipulation for per-segment (where the parser nests nodes).
|
|
127
|
+
*/
|
|
128
|
+
export function commentLineContent(content: string, erbNodes: ERBContentNode[], strategy: CommentStrategy, parserService: ParserService): string {
|
|
129
|
+
if (strategy === "per-segment") {
|
|
130
|
+
return commentPerSegment(content, erbNodes)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parseResult = parserService.parseContent(content, { track_whitespace: true })
|
|
134
|
+
const lineCollector = new LineContextCollector()
|
|
135
|
+
parseResult.visit(lineCollector)
|
|
136
|
+
|
|
137
|
+
const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || []
|
|
138
|
+
const document = parseResult.value
|
|
139
|
+
const children = asMutable(document).children
|
|
140
|
+
|
|
141
|
+
switch (strategy) {
|
|
142
|
+
case "all-erb":
|
|
143
|
+
for (const node of lineERBNodes) {
|
|
144
|
+
commentERBNode(node)
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
case "whole-line": {
|
|
149
|
+
for (const node of lineERBNodes) {
|
|
150
|
+
commentERBNode(node)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const commentNode = new HTMLCommentNode({
|
|
154
|
+
type: "AST_HTML_COMMENT_NODE",
|
|
155
|
+
location: Location.zero,
|
|
156
|
+
errors: [],
|
|
157
|
+
comment_start: createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
|
|
158
|
+
children: [createLiteral(" "), ...(children.slice() as Node[]), createLiteral(" ")],
|
|
159
|
+
comment_end: createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
children.length = 0
|
|
163
|
+
children.push(commentNode)
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "html-only": {
|
|
168
|
+
const commentNode = new HTMLCommentNode({
|
|
169
|
+
type: "AST_HTML_COMMENT_NODE",
|
|
170
|
+
location: Location.zero,
|
|
171
|
+
errors: [],
|
|
172
|
+
comment_start: createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
|
|
173
|
+
children: [createLiteral(" "), ...(children.slice() as Node[]), createLiteral(" ")],
|
|
174
|
+
comment_end: createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
children.length = 0
|
|
178
|
+
children.push(commentNode)
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return IdentityPrinter.print(document, { ignoreErrors: true })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Per-segment commenting uses text segments because the parser creates nested
|
|
188
|
+
* structures (e.g., ERBIfNode) that don't allow flat child iteration.
|
|
189
|
+
*/
|
|
190
|
+
function commentPerSegment(content: string, erbNodes: ERBContentNode[]): string {
|
|
191
|
+
const segments = getLineSegments(content, erbNodes)
|
|
192
|
+
|
|
193
|
+
return segments.map(segment => {
|
|
194
|
+
if (segment.isERB) {
|
|
195
|
+
return segment.text.substring(0, 2) + "#" + segment.text.substring(2)
|
|
196
|
+
} else if (segment.text.trim() !== "") {
|
|
197
|
+
return `<!-- ${segment.text} -->`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return segment.text
|
|
201
|
+
}).join("")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function uncommentLineContent(content: string, parserService: ParserService): string {
|
|
205
|
+
const parseResult = parserService.parseContent(content, { track_whitespace: true })
|
|
206
|
+
const lineCollector = new LineContextCollector()
|
|
207
|
+
|
|
208
|
+
parseResult.visit(lineCollector)
|
|
209
|
+
|
|
210
|
+
const lineERBNodes = lineCollector.erbNodesPerLine.get(0) || []
|
|
211
|
+
const document = parseResult.value
|
|
212
|
+
const children = asMutable(document).children
|
|
213
|
+
|
|
214
|
+
for (const node of lineERBNodes) {
|
|
215
|
+
if (isERBCommentNode(node)) {
|
|
216
|
+
uncommentERBNode(node)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let index = 0
|
|
221
|
+
|
|
222
|
+
while (index < children.length) {
|
|
223
|
+
const child = children[index]
|
|
224
|
+
|
|
225
|
+
if (child.type === "AST_HTML_COMMENT_NODE") {
|
|
226
|
+
const commentNode = child as HTMLCommentNode
|
|
227
|
+
const innerChildren = [...commentNode.children]
|
|
228
|
+
|
|
229
|
+
if (innerChildren.length > 0) {
|
|
230
|
+
const first = innerChildren[0]
|
|
231
|
+
|
|
232
|
+
if (isLiteralNode(first) && first.content.startsWith(" ")) {
|
|
233
|
+
const trimmed = first.content.substring(1)
|
|
234
|
+
|
|
235
|
+
if (trimmed === "") {
|
|
236
|
+
innerChildren.shift()
|
|
237
|
+
} else {
|
|
238
|
+
innerChildren[0] = createLiteral(trimmed)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (innerChildren.length > 0) {
|
|
244
|
+
const last = innerChildren[innerChildren.length - 1]
|
|
245
|
+
|
|
246
|
+
if (isLiteralNode(last) && last.content.endsWith(" ")) {
|
|
247
|
+
const trimmed = last.content.substring(0, last.content.length - 1)
|
|
248
|
+
|
|
249
|
+
if (trimmed === "") {
|
|
250
|
+
innerChildren.pop()
|
|
251
|
+
} else {
|
|
252
|
+
innerChildren[innerChildren.length - 1] = createLiteral(trimmed)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const innerERBNodes: ERBContentNode[] = []
|
|
258
|
+
const innerCollector = new LineContextCollector()
|
|
259
|
+
|
|
260
|
+
for (const innerChild of innerChildren) {
|
|
261
|
+
innerCollector.visit(innerChild)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
innerERBNodes.push(...(innerCollector.erbNodesPerLine.get(0) || []))
|
|
265
|
+
|
|
266
|
+
for (const erbNode of innerERBNodes) {
|
|
267
|
+
if (isERBCommentNode(erbNode)) {
|
|
268
|
+
uncommentERBNode(erbNode)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
children.splice(index, 1, ...innerChildren)
|
|
273
|
+
index += innerChildren.length
|
|
274
|
+
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
index++
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return IdentityPrinter.print(document, { ignoreErrors: true })
|
|
282
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { TextEdit, Range, Position } from "vscode-languageserver/node"
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
|
+
|
|
4
|
+
import { ParserService } from "./parser_service"
|
|
5
|
+
import { LineContextCollector } from "./line_context_collector"
|
|
6
|
+
|
|
7
|
+
import { lspLine } from "./range_utils"
|
|
8
|
+
import { determineStrategy, commentLineContent, uncommentLineContent } from "./comment_ast_utils"
|
|
9
|
+
|
|
10
|
+
import type { LineInfo } from "./line_context_collector"
|
|
11
|
+
import type { ERBContentNode, HTMLCommentNode } from "@herb-tools/core"
|
|
12
|
+
|
|
13
|
+
export class CommentService {
|
|
14
|
+
private parserService: ParserService
|
|
15
|
+
|
|
16
|
+
constructor(parserService: ParserService) {
|
|
17
|
+
this.parserService = parserService
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
toggleLineComment(document: TextDocument, range: Range): TextEdit[] {
|
|
21
|
+
const parseResult = this.parserService.parseDocument(document)
|
|
22
|
+
const collector = new LineContextCollector()
|
|
23
|
+
|
|
24
|
+
collector.visit(parseResult.document)
|
|
25
|
+
|
|
26
|
+
const startLine = range.start.line
|
|
27
|
+
const endLine = range.end.line
|
|
28
|
+
const lineInfos: LineInfo[] = []
|
|
29
|
+
|
|
30
|
+
for (let line = startLine; line <= endLine; line++) {
|
|
31
|
+
const lineText = document.getText(Range.create(line, 0, line + 1, 0)).replace(/\n$/, "")
|
|
32
|
+
|
|
33
|
+
if (lineText.trim() === "") {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.lineIsIfFalseWrapped(lineText) !== null) {
|
|
38
|
+
lineInfos.push({ line, context: "erb-comment", node: null })
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const htmlCommentNode = collector.htmlCommentNodesPerLine.get(line)
|
|
43
|
+
const info = collector.lineMap.get(line)
|
|
44
|
+
|
|
45
|
+
if (htmlCommentNode && this.htmlCommentSpansLine(htmlCommentNode, lineText)) {
|
|
46
|
+
lineInfos.push({ line, context: "html-comment", node: htmlCommentNode })
|
|
47
|
+
} else if (info) {
|
|
48
|
+
if (info.context === "html-comment") {
|
|
49
|
+
lineInfos.push({ line, context: "html-content", node: null })
|
|
50
|
+
} else {
|
|
51
|
+
lineInfos.push(info)
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
lineInfos.push({ line, context: "html-content", node: null })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (lineInfos.length === 0) return []
|
|
59
|
+
|
|
60
|
+
const allCommented = lineInfos.every(
|
|
61
|
+
info => info.context === "erb-comment" || info.context === "html-comment"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const edits: TextEdit[] = []
|
|
65
|
+
|
|
66
|
+
if (allCommented) {
|
|
67
|
+
for (const info of lineInfos) {
|
|
68
|
+
const lineText = document.getText(Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "")
|
|
69
|
+
const edit = this.uncommentLine(info, lineText, collector)
|
|
70
|
+
|
|
71
|
+
if (edit) edits.push(edit)
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (const info of lineInfos) {
|
|
75
|
+
if (info.context === "erb-comment" || info.context === "html-comment") continue
|
|
76
|
+
|
|
77
|
+
const lineText = document.getText(Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "")
|
|
78
|
+
const erbNodes = collector.erbNodesPerLine.get(info.line) || []
|
|
79
|
+
const edit = this.commentLine(info, lineText, erbNodes, collector)
|
|
80
|
+
|
|
81
|
+
if (edit) edits.push(edit)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return edits
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
toggleBlockComment(document: TextDocument, range: Range): TextEdit[] {
|
|
89
|
+
const startLine = range.start.line
|
|
90
|
+
const endLine = range.end.line
|
|
91
|
+
|
|
92
|
+
const firstLineText = document.getText(Range.create(startLine, 0, startLine + 1, 0)).replace(/\n$/, "")
|
|
93
|
+
const lastLineText = document.getText(Range.create(endLine, 0, endLine + 1, 0)).replace(/\n$/, "")
|
|
94
|
+
const isWrapped = firstLineText.trim() === "<% if false %>" && lastLineText.trim() === "<% end %>"
|
|
95
|
+
|
|
96
|
+
if (isWrapped) {
|
|
97
|
+
return [
|
|
98
|
+
TextEdit.del(Range.create(endLine, 0, endLine + 1, 0)),
|
|
99
|
+
TextEdit.del(Range.create(startLine, 0, startLine + 1, 0)),
|
|
100
|
+
]
|
|
101
|
+
} else {
|
|
102
|
+
const firstLineIndent = this.getIndentation(firstLineText)
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
TextEdit.insert(Position.create(endLine + 1, 0), `${firstLineIndent}<% end %>\n`),
|
|
106
|
+
TextEdit.insert(Position.create(startLine, 0), `${firstLineIndent}<% if false %>\n`),
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private commentLine(info: LineInfo, lineText: string, erbNodes: ERBContentNode[], collector: LineContextCollector): TextEdit | null {
|
|
112
|
+
const lineRange = Range.create(info.line, 0, info.line, lineText.length)
|
|
113
|
+
const indent = this.getIndentation(lineText)
|
|
114
|
+
const content = lineText.trimStart()
|
|
115
|
+
const htmlCommentNode = collector.htmlCommentNodesPerLine.get(info.line)
|
|
116
|
+
|
|
117
|
+
if (htmlCommentNode) {
|
|
118
|
+
return TextEdit.replace(lineRange, `${indent}<% if false %>${content}<% end %>`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const strategy = determineStrategy(erbNodes, lineText)
|
|
122
|
+
|
|
123
|
+
if (strategy === "single-erb") {
|
|
124
|
+
const node = erbNodes[0]
|
|
125
|
+
const insertColumn = node.tag_opening!.location.start.column + 2
|
|
126
|
+
|
|
127
|
+
return TextEdit.insert(Position.create(info.line, insertColumn), "#")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = commentLineContent(content, erbNodes, strategy, this.parserService)
|
|
131
|
+
|
|
132
|
+
return TextEdit.replace(lineRange, indent + result)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private lineIsIfFalseWrapped(lineText: string): string | null {
|
|
136
|
+
const trimmed = lineText.trimStart()
|
|
137
|
+
const indent = this.getIndentation(lineText)
|
|
138
|
+
|
|
139
|
+
if (trimmed.startsWith("<% if false %>") && trimmed.endsWith("<% end %>")) {
|
|
140
|
+
const inner = trimmed.slice("<% if false %>".length, -"<% end %>".length)
|
|
141
|
+
|
|
142
|
+
return indent + inner
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private uncommentLine(info: LineInfo, lineText: string, collector: LineContextCollector): TextEdit | null {
|
|
149
|
+
const lineRange = Range.create(info.line, 0, info.line, lineText.length)
|
|
150
|
+
const indent = this.getIndentation(lineText)
|
|
151
|
+
const ifFalseContent = this.lineIsIfFalseWrapped(lineText)
|
|
152
|
+
|
|
153
|
+
if (ifFalseContent !== null) {
|
|
154
|
+
return TextEdit.replace(lineRange, ifFalseContent)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (info.context === "erb-comment") {
|
|
158
|
+
const node = info.node as ERBContentNode
|
|
159
|
+
if (!node?.tag_opening || !node?.tag_closing) return null
|
|
160
|
+
|
|
161
|
+
const contentValue = (node as any).content?.value as string | null
|
|
162
|
+
const trimmedContent = contentValue?.trim() || ""
|
|
163
|
+
|
|
164
|
+
if (trimmedContent.startsWith("<") && !trimmedContent.startsWith("<%")) {
|
|
165
|
+
return TextEdit.replace(lineRange, `${indent}${trimmedContent}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (lspLine(node.tag_opening.location.start) !== info.line) return null
|
|
169
|
+
|
|
170
|
+
const erbNodes = collector.erbNodesPerLine.get(info.line) || []
|
|
171
|
+
|
|
172
|
+
if (erbNodes.length > 1) {
|
|
173
|
+
const content = lineText.trimStart()
|
|
174
|
+
const result = uncommentLineContent(content, this.parserService)
|
|
175
|
+
|
|
176
|
+
return TextEdit.replace(lineRange, indent + result)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hashColumn = node.tag_opening.location.start.column + 2
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
contentValue?.startsWith(" graphql ") ||
|
|
183
|
+
contentValue?.startsWith(" %= ") ||
|
|
184
|
+
contentValue?.startsWith(" == ") ||
|
|
185
|
+
contentValue?.startsWith(" % ") ||
|
|
186
|
+
contentValue?.startsWith(" = ") ||
|
|
187
|
+
contentValue?.startsWith(" - ")
|
|
188
|
+
) {
|
|
189
|
+
return TextEdit.del(Range.create(info.line, hashColumn, info.line, hashColumn + 2))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return TextEdit.del(Range.create(info.line, hashColumn, info.line, hashColumn + 1))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (info.context === "html-comment") {
|
|
196
|
+
const commentNode = info.node as HTMLCommentNode | null
|
|
197
|
+
|
|
198
|
+
if (commentNode?.comment_start && commentNode?.comment_end) {
|
|
199
|
+
const contentStart = commentNode.comment_start.location.end.column
|
|
200
|
+
const contentEnd = commentNode.comment_end.location.start.column
|
|
201
|
+
const innerContent = lineText.substring(contentStart, contentEnd).trim()
|
|
202
|
+
|
|
203
|
+
const result = uncommentLineContent(innerContent, this.parserService)
|
|
204
|
+
|
|
205
|
+
return TextEdit.replace(lineRange, `${indent}${result}`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private htmlCommentSpansLine(node: HTMLCommentNode, lineText: string): boolean {
|
|
213
|
+
if (!node.comment_start || !node.comment_end) return false
|
|
214
|
+
|
|
215
|
+
const commentStart = node.comment_start.location.start.column
|
|
216
|
+
const commentEnd = node.comment_end.location.end.column
|
|
217
|
+
const contentBefore = lineText.substring(0, commentStart).trim()
|
|
218
|
+
const contentAfter = lineText.substring(commentEnd).trim()
|
|
219
|
+
|
|
220
|
+
return contentBefore === "" && contentAfter === ""
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private getIndentation(lineText: string): string {
|
|
224
|
+
const match = lineText.match(/^(\s*)/)
|
|
225
|
+
|
|
226
|
+
return match ? match[1] : ""
|
|
227
|
+
}
|
|
228
|
+
}
|