@herb-tools/language-server 0.3.1 → 0.4.1

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.
Files changed (52) hide show
  1. package/README.md +74 -7
  2. package/bin/herb-language-server +0 -16
  3. package/dist/cli.js +27 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/diagnostics.js +17 -50
  7. package/dist/diagnostics.js.map +1 -1
  8. package/dist/formatting_service.js +124 -0
  9. package/dist/formatting_service.js.map +1 -0
  10. package/dist/herb-language-server.js +16118 -6762
  11. package/dist/herb-language-server.js.map +1 -1
  12. package/dist/index.cjs +29900 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.js +27 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/linter_service.js +44 -0
  17. package/dist/linter_service.js.map +1 -0
  18. package/dist/parser_service.js +47 -0
  19. package/dist/parser_service.js.map +1 -0
  20. package/dist/project.js +2 -1
  21. package/dist/project.js.map +1 -1
  22. package/dist/server.js +72 -61
  23. package/dist/server.js.map +1 -1
  24. package/dist/service.js +16 -4
  25. package/dist/service.js.map +1 -1
  26. package/dist/settings.js +11 -1
  27. package/dist/settings.js.map +1 -1
  28. package/dist/types/cli.d.ts +4 -0
  29. package/dist/types/config.d.ts +9 -1
  30. package/dist/types/diagnostics.d.ts +9 -7
  31. package/dist/types/formatting_service.d.ts +19 -0
  32. package/dist/types/herb-language-server.d.ts +2 -0
  33. package/dist/types/index.d.ts +10 -0
  34. package/dist/types/linter_service.d.ts +14 -0
  35. package/dist/types/parser_service.d.ts +10 -0
  36. package/dist/types/project.d.ts +2 -0
  37. package/dist/types/server.d.ts +7 -1
  38. package/dist/types/service.d.ts +6 -0
  39. package/dist/types/settings.d.ts +11 -0
  40. package/package.json +22 -24
  41. package/src/cli.ts +23 -0
  42. package/src/config.ts +9 -1
  43. package/src/diagnostics.ts +22 -79
  44. package/src/formatting_service.ts +150 -0
  45. package/src/herb-language-server.ts +6 -0
  46. package/src/index.ts +10 -0
  47. package/src/linter_service.ts +63 -0
  48. package/src/parser_service.ts +59 -0
  49. package/src/project.ts +4 -2
  50. package/src/server.ts +83 -65
  51. package/src/service.ts +21 -6
  52. package/src/settings.ts +24 -2
package/package.json CHANGED
@@ -1,41 +1,42 @@
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.3.1",
4
+ "version": "0.4.1",
5
5
  "author": "Marco Roth",
6
6
  "license": "MIT",
7
7
  "engines": {
8
8
  "node": "*"
9
9
  },
10
+ "homepage": "https://herb-tools.dev",
10
11
  "bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/language-server%60:%20",
11
12
  "repository": {
12
13
  "type": "git",
13
14
  "url": "https://github.com/marcoroth/herb.git",
14
15
  "directory": "javascript/packages/language-server"
15
16
  },
16
- "main": "./dist/herb-language-server.cjs",
17
- "module": "./dist/server.js",
18
- "types": "./dist/types/server.d.ts",
17
+ "bin": {
18
+ "herb-language-server": "./bin/herb-language-server"
19
+ },
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "require": "./dist/index.cjs",
23
+ "types": "./dist/types/index.d.ts",
19
24
  "exports": {
20
25
  "./package.json": "./package.json",
21
26
  ".": {
22
- "types": "./dist/types/server.d.ts",
23
- "import": "./dist/server.js",
24
- "require": "./dist/herb-language-server.cjs",
25
- "default": "./dist/server.js"
27
+ "types": "./dist/types/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs",
30
+ "default": "./dist/index.js"
26
31
  }
27
32
  },
28
- "homepage": "https://herb-tools.dev",
29
- "bin": {
30
- "herb-language-server": "./bin/herb-language-server"
31
- },
32
33
  "scripts": {
33
34
  "clean": "rimraf dist",
34
- "prebuild": "yarn run clean",
35
- "build": "tsc -b && rollup -c rollup.config.mjs",
36
- "watch": "tsc -b -w",
37
- "test": "echo 'TODO: add tests'",
38
- "prepublishOnly": "yarn clean && yarn build && yarn test"
35
+ "build": "yarn clean && tsc -b && rollup -c",
36
+ "dev": "tsc -b -w",
37
+ "test": "vitest",
38
+ "test:run": "vitest run",
39
+ "prepublishOnly": "yarn clean && yarn build && yarn test:run"
39
40
  },
40
41
  "files": [
41
42
  "package.json",
@@ -45,17 +46,14 @@
45
46
  "dist/"
46
47
  ],
47
48
  "dependencies": {
48
- "@herb-tools/node-wasm": "0.3.1",
49
+ "@herb-tools/formatter": "0.4.1",
50
+ "@herb-tools/linter": "0.4.1",
51
+ "@herb-tools/node-wasm": "0.4.1",
49
52
  "dedent": "^1.6.0",
50
- "typescript": "^5.8.3",
51
53
  "vscode-languageserver": "^9.0.1",
52
54
  "vscode-languageserver-textdocument": "^1.0.12"
53
55
  },
54
56
  "devDependencies": {
55
- "@rollup/plugin-commonjs": "^28.0.6",
56
- "@rollup/plugin-typescript": "^12.1.3",
57
- "@rollup/plugin-node-resolve": "^16.0.1",
58
- "@rollup/plugin-json": "^6.1.0",
59
- "rimraf": "^6.0.1"
57
+ "vitest": "^1.0.0"
60
58
  }
61
59
  }
package/src/cli.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Server } from "./server"
2
+
3
+ export class CLI {
4
+ private usage = `
5
+ Usage: herb-language-server [options]
6
+
7
+ Options:
8
+ --stdio use stdio
9
+ --node-ipc use node-ipc
10
+ --socket=<port> use socket
11
+ `
12
+
13
+ run() {
14
+ if (process.argv.length <= 2) {
15
+ console.error(`Error: Connection input stream is not set. Set command line parameters: '--node-ipc', '--stdio' or '--socket=<port>'`)
16
+ console.error(this.usage)
17
+ process.exit(1)
18
+ }
19
+
20
+ const server = new Server()
21
+ server.listen()
22
+ }
23
+ }
package/src/config.ts CHANGED
@@ -1,4 +1,12 @@
1
- export type HerbConfigOptions = {}
1
+ export type HerbConfigOptions = {
2
+ formatter?: {
3
+ enabled?: boolean
4
+ include?: string[]
5
+ exclude?: string[]
6
+ indentWidth?: number
7
+ maxLineLength?: number
8
+ }
9
+ }
2
10
 
3
11
  export type HerbLSPConfig = {
4
12
  version: string
@@ -1,106 +1,49 @@
1
- import { Connection, Diagnostic, DiagnosticSeverity, Range, Position } from "vscode-languageserver/node"
2
1
  import { TextDocument } from "vscode-languageserver-textdocument"
3
- import { Herb, Visitor } from "@herb-tools/node-wasm"
2
+ import { Connection, Diagnostic } from "vscode-languageserver/node"
4
3
 
4
+ import { ParserService } from "./parser_service"
5
+ import { LinterService } from "./linter_service"
5
6
  import { DocumentService } from "./document_service"
6
7
 
7
- import type { Node, HerbError } from "@herb-tools/node-wasm"
8
-
9
- class ErrorVisitor extends Visitor {
10
- private diagnostics: Diagnostics
11
- private textDocument: TextDocument
12
-
13
- constructor(diagnostics: Diagnostics, textDocument: TextDocument) {
14
- super()
15
- this.diagnostics = diagnostics
16
- this.textDocument = textDocument
17
- }
18
-
19
- visitChildNodes(node: Node) {
20
- super.visitChildNodes(node)
21
-
22
- node.errors.forEach(error => this.publishDiagnosticForError(error, node))
23
- }
24
-
25
- private publishDiagnosticForError(error: HerbError, node: Node): void {
26
- this.diagnostics.pushDiagnostic(
27
- error.message,
28
- error.type,
29
- this.rangeFromHerbError(error),
30
- this.textDocument,
31
- {
32
- error: error.toJSON(),
33
- node: node.toJSON()
34
- },
35
- DiagnosticSeverity.Error
36
- )
37
- }
38
-
39
- private rangeFromHerbError(error: HerbError): Range {
40
- return Range.create(
41
- Position.create(error.location.start.line - 1, error.location.start.column),
42
- Position.create(error.location.end.line - 1, error.location.end.column),
43
- )
44
- }
45
- }
46
-
47
8
  export class Diagnostics {
48
9
  private readonly connection: Connection
49
10
  private readonly documentService: DocumentService
50
- private readonly diagnosticsSource = "Herb LSP "
11
+ private readonly parserService: ParserService
12
+ private readonly linterService: LinterService
51
13
  private diagnostics: Map<TextDocument, Diagnostic[]> = new Map()
52
14
 
53
15
  constructor(
54
16
  connection: Connection,
55
17
  documentService: DocumentService,
18
+ parserService: ParserService,
19
+ linterService: LinterService,
56
20
  ) {
57
21
  this.connection = connection
58
22
  this.documentService = documentService
23
+ this.parserService = parserService
24
+ this.linterService = linterService
59
25
  }
60
26
 
61
- validate(textDocument: TextDocument) {
62
- const content = textDocument.getText()
63
- const result = Herb.parse(content)
64
- const visitor = new ErrorVisitor(this, textDocument)
27
+ async validate(textDocument: TextDocument) {
28
+ const parseResult = this.parserService.parseDocument(textDocument)
29
+ const lintResult = await this.linterService.lintDocument(parseResult.document, textDocument)
65
30
 
66
- result.visit(visitor)
31
+ const allDiagnostics = [
32
+ ...parseResult.diagnostics,
33
+ ...lintResult.diagnostics,
34
+ ]
67
35
 
36
+ this.diagnostics.set(textDocument, allDiagnostics)
68
37
  this.sendDiagnosticsFor(textDocument)
69
38
  }
70
39
 
71
- refreshDocument(document: TextDocument) {
72
- this.validate(document)
73
- }
74
-
75
- refreshAllDocuments() {
76
- this.documentService.getAll().forEach((document) => {
77
- this.refreshDocument(document)
78
- })
40
+ async refreshDocument(document: TextDocument) {
41
+ await this.validate(document)
79
42
  }
80
43
 
81
- pushDiagnostic(
82
- message: string,
83
- code: string,
84
- range: Range,
85
- textDocument: TextDocument,
86
- data = {},
87
- severity: DiagnosticSeverity = DiagnosticSeverity.Error,
88
- ) {
89
- const diagnostic: Diagnostic = {
90
- source: this.diagnosticsSource,
91
- severity,
92
- range,
93
- message,
94
- code,
95
- data,
96
- }
97
-
98
- const diagnostics = this.diagnostics.get(textDocument) || []
99
- diagnostics.push(diagnostic)
100
-
101
- this.diagnostics.set(textDocument, diagnostics)
102
-
103
- return diagnostic
44
+ async refreshAllDocuments() {
45
+ const documents = this.documentService.getAll()
46
+ await Promise.all(documents.map(document => this.refreshDocument(document)))
104
47
  }
105
48
 
106
49
  private sendDiagnosticsFor(textDocument: TextDocument) {
@@ -0,0 +1,150 @@
1
+ import { Connection, TextDocuments, DocumentFormattingParams, TextEdit, Range, Position } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+ import { Formatter, defaultFormatOptions } from "@herb-tools/formatter"
4
+ import { Project } from "./project"
5
+ import { Settings } from "./settings"
6
+ import { Config } from "./config"
7
+ import { glob } from "glob"
8
+
9
+ export class FormattingService {
10
+ private connection: Connection
11
+ private documents: TextDocuments<TextDocument>
12
+ private project: Project
13
+ private settings: Settings
14
+ private config?: Config
15
+
16
+ constructor(connection: Connection, documents: TextDocuments<TextDocument>, project: Project, settings: Settings) {
17
+ this.connection = connection
18
+ this.documents = documents
19
+ this.project = project
20
+ this.settings = settings
21
+ }
22
+
23
+ async initialize() {
24
+ try {
25
+ this.config = await Config.fromPathOrNew(this.project.projectPath)
26
+ this.connection.console.log("Herb formatter initialized successfully")
27
+ } catch (error) {
28
+ this.connection.console.error(`Failed to initialize Herb formatter: ${error}`)
29
+ }
30
+ }
31
+
32
+ async refreshConfig() {
33
+ this.config = await Config.fromPathOrNew(this.project.projectPath)
34
+ }
35
+
36
+ private async shouldFormatFile(filePath: string): Promise<boolean> {
37
+ if (!this.config?.options.formatter) {
38
+ return true
39
+ }
40
+
41
+ const formatter = this.config.options.formatter
42
+
43
+ // Check if formatting is disabled in project config
44
+ if (formatter.enabled === false) {
45
+ return false
46
+ }
47
+
48
+ // Check exclude patterns first
49
+ if (formatter.exclude) {
50
+ for (const pattern of formatter.exclude) {
51
+ try {
52
+ const matches = await new Promise<string[]>((resolve, reject) => {
53
+ glob(pattern, { cwd: this.project.projectPath }).then(resolve).catch(reject)
54
+ })
55
+
56
+ if (Array.isArray(matches) && matches.some((match: string) => filePath.includes(match) || filePath.endsWith(match))) {
57
+ return false
58
+ }
59
+ } catch (error) {
60
+ continue
61
+ }
62
+ }
63
+ }
64
+
65
+ if (formatter.include && formatter.include.length > 0) {
66
+ for (const pattern of formatter.include) {
67
+ try {
68
+ const matches = await new Promise<string[]>((resolve, reject) => {
69
+ glob(pattern, { cwd: this.project.projectPath }).then(resolve).catch(reject)
70
+ })
71
+ if (Array.isArray(matches) && matches.some((match: string) => filePath.includes(match) || filePath.endsWith(match))) {
72
+ return true
73
+ }
74
+ } catch (error) {
75
+ continue
76
+ }
77
+ }
78
+
79
+ return false
80
+ }
81
+
82
+ return true
83
+ }
84
+
85
+ private async getFormatterOptions(uri: string) {
86
+ const settings = await this.settings.getDocumentSettings(uri)
87
+
88
+ const projectFormatter = this.config?.options.formatter || {}
89
+
90
+ return {
91
+ indentWidth: projectFormatter.indentWidth ?? settings.formatter?.indentWidth ?? defaultFormatOptions.indentWidth,
92
+ maxLineLength: projectFormatter.maxLineLength ?? settings.formatter?.maxLineLength ?? defaultFormatOptions.maxLineLength
93
+ }
94
+ }
95
+
96
+ private async performFormatting(params: DocumentFormattingParams): Promise<TextEdit[]> {
97
+ const document = this.documents.get(params.textDocument.uri)
98
+
99
+ if (!document) {
100
+ return []
101
+ }
102
+
103
+ try {
104
+ const options = await this.getFormatterOptions(params.textDocument.uri)
105
+ const formatter = new Formatter(this.project.herbBackend, options)
106
+
107
+ const text = document.getText()
108
+ let newText = formatter.format(text)
109
+
110
+ if (!newText.endsWith('\n')) {
111
+ newText = newText + '\n'
112
+ }
113
+
114
+ if (newText === text) {
115
+ return []
116
+ }
117
+
118
+ const range: Range = {
119
+ start: Position.create(0, 0),
120
+ end: Position.create(document.lineCount, 0)
121
+ }
122
+
123
+ return [{ range, newText }]
124
+ } catch (error) {
125
+ this.connection.console.error(`Formatting failed: ${error}`)
126
+
127
+ return []
128
+ }
129
+ }
130
+
131
+ async formatDocument(params: DocumentFormattingParams): Promise<TextEdit[]> {
132
+ const settings = await this.settings.getDocumentSettings(params.textDocument.uri)
133
+
134
+ if (settings.formatter?.enabled === false) {
135
+ return []
136
+ }
137
+
138
+ const filePath = params.textDocument.uri.replace(/^file:\/\//, '')
139
+
140
+ if (!(await this.shouldFormatFile(filePath))) {
141
+ return []
142
+ }
143
+
144
+ return this.performFormatting(params)
145
+ }
146
+
147
+ async formatDocumentIgnoreConfig(params: DocumentFormattingParams): Promise<TextEdit[]> {
148
+ return this.performFormatting(params)
149
+ }
150
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { CLI } from "./cli"
4
+
5
+ const cli = new CLI()
6
+ cli.run()
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./server"
2
+ export * from "./service"
3
+ export * from "./config"
4
+ export * from "./diagnostics"
5
+ export * from "./document_service"
6
+ export * from "./formatting_service"
7
+ export * from "./project"
8
+ export * from "./settings"
9
+ export * from "./utils"
10
+ export * from "./cli"
@@ -0,0 +1,63 @@
1
+ import { Diagnostic, DiagnosticSeverity, Range, Position, CodeDescription } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+ import { Linter } from "@herb-tools/linter"
4
+
5
+ import { Settings } from "./settings"
6
+
7
+ import type { DocumentNode } from "@herb-tools/node-wasm"
8
+
9
+ export interface LintServiceResult {
10
+ diagnostics: Diagnostic[]
11
+ }
12
+
13
+ export class LinterService {
14
+ private readonly settings: Settings
15
+ private readonly source = "Herb Linter "
16
+ private linter: Linter
17
+
18
+ constructor(settings: Settings) {
19
+ this.settings = settings
20
+ this.linter = new Linter()
21
+ }
22
+
23
+ async lintDocument(document: DocumentNode, textDocument: TextDocument): Promise<LintServiceResult> {
24
+ const settings = await this.settings.getDocumentSettings(textDocument.uri)
25
+ const linterEnabled = settings?.linter?.enabled ?? true
26
+
27
+ if (!linterEnabled) {
28
+ return { diagnostics: [] }
29
+ }
30
+
31
+ const lintResult = this.linter.lint(document)
32
+ const diagnostics: Diagnostic[] = []
33
+
34
+ lintResult.offenses.forEach(offense => {
35
+ const severity = offense.severity === "error"
36
+ ? DiagnosticSeverity.Error
37
+ : DiagnosticSeverity.Warning
38
+
39
+ const range = Range.create(
40
+ Position.create(offense.location.start.line - 1, offense.location.start.column),
41
+ Position.create(offense.location.end.line - 1, offense.location.end.column),
42
+ )
43
+
44
+ const codeDescription: CodeDescription = {
45
+ href: `https://herb-tools.dev/linter/rules/${offense.rule}`
46
+ }
47
+
48
+ const diagnostic: Diagnostic = {
49
+ source: this.source,
50
+ severity,
51
+ range,
52
+ message: offense.message,
53
+ code: offense.rule,
54
+ data: { rule: offense.rule },
55
+ codeDescription
56
+ }
57
+
58
+ diagnostics.push(diagnostic)
59
+ })
60
+
61
+ return { diagnostics }
62
+ }
63
+ }
@@ -0,0 +1,59 @@
1
+ import { Diagnostic, DiagnosticSeverity, Range, Position } from "vscode-languageserver/node"
2
+ import { TextDocument } from "vscode-languageserver-textdocument"
3
+ import { Herb, Visitor } from "@herb-tools/node-wasm"
4
+
5
+ import type { Node, HerbError, DocumentNode } from "@herb-tools/node-wasm"
6
+
7
+ class ErrorVisitor extends Visitor {
8
+ private readonly source = "Herb Parser "
9
+ public diagnostics: Diagnostic[] = []
10
+
11
+ visitChildNodes(node: Node) {
12
+ super.visitChildNodes(node)
13
+
14
+ node.errors.forEach(error => this.addDiagnosticForError(error, node))
15
+ }
16
+
17
+ private addDiagnosticForError(error: HerbError, node: Node): void {
18
+ const diagnostic: Diagnostic = {
19
+ source: this.source,
20
+ severity: DiagnosticSeverity.Error,
21
+ range: this.rangeFromHerbError(error),
22
+ message: error.message,
23
+ code: error.type,
24
+ data: {
25
+ error: error.toJSON(),
26
+ node: node.toJSON()
27
+ }
28
+ }
29
+
30
+ this.diagnostics.push(diagnostic)
31
+ }
32
+
33
+ private rangeFromHerbError(error: HerbError): Range {
34
+ return Range.create(
35
+ Position.create(error.location.start.line - 1, error.location.start.column),
36
+ Position.create(error.location.end.line - 1, error.location.end.column),
37
+ )
38
+ }
39
+ }
40
+
41
+ export interface ParseServiceResult {
42
+ document: DocumentNode
43
+ diagnostics: Diagnostic[]
44
+ }
45
+
46
+ export class ParserService {
47
+ parseDocument(textDocument: TextDocument): ParseServiceResult {
48
+ const content = textDocument.getText()
49
+ const result = Herb.parse(content)
50
+
51
+ const errorVisitor = new ErrorVisitor()
52
+ result.visit(errorVisitor)
53
+
54
+ return {
55
+ document: result.value,
56
+ diagnostics: errorVisitor.diagnostics
57
+ }
58
+ }
59
+ }
package/src/project.ts CHANGED
@@ -1,17 +1,19 @@
1
- import { Herb } from "@herb-tools/node-wasm"
1
+ import { Herb, HerbBackend } from "@herb-tools/node-wasm"
2
2
  import { Connection } from "vscode-languageserver/node"
3
3
 
4
4
  export class Project {
5
5
  connection: Connection
6
6
  projectPath: string
7
+ herbBackend: HerbBackend
7
8
 
8
9
  constructor(connection: Connection, projectPath: string) {
9
10
  this.projectPath = projectPath
10
11
  this.connection = connection
12
+ this.herbBackend = Herb
11
13
  }
12
14
 
13
15
  async initialize() {
14
- await Herb.load()
16
+ await this.herbBackend.load()
15
17
  }
16
18
 
17
19
  async refresh() {