@herb-tools/language-server 0.3.0 → 0.4.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/CHANGELOG.md +4 -0
- package/README.md +81 -8
- package/bin/herb-language-server +3 -0
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostics.js +17 -50
- package/dist/diagnostics.js.map +1 -1
- package/dist/formatting_service.js +126 -0
- package/dist/formatting_service.js.map +1 -0
- package/dist/herb-language-server.js +15260 -6068
- package/dist/herb-language-server.js.map +1 -1
- package/dist/index.cjs +29736 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/linter_service.js +44 -0
- package/dist/linter_service.js.map +1 -0
- package/dist/parser_service.js +47 -0
- package/dist/parser_service.js.map +1 -0
- package/dist/project.js +2 -1
- package/dist/project.js.map +1 -1
- package/dist/server.js +72 -61
- package/dist/server.js.map +1 -1
- package/dist/service.js +16 -4
- package/dist/service.js.map +1 -1
- package/dist/settings.js +8 -1
- package/dist/settings.js.map +1 -1
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +9 -1
- package/dist/types/diagnostics.d.ts +9 -7
- package/dist/types/formatting_service.d.ts +19 -0
- package/dist/types/herb-language-server.d.ts +2 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/linter_service.d.ts +14 -0
- package/dist/types/parser_service.d.ts +10 -0
- package/dist/types/project.d.ts +2 -0
- package/dist/types/server.d.ts +7 -1
- package/dist/types/service.d.ts +6 -0
- package/dist/types/settings.d.ts +11 -0
- package/package.json +28 -19
- package/src/cli.ts +23 -0
- package/src/config.ts +109 -0
- package/src/diagnostics.ts +59 -0
- package/src/document_service.ts +35 -0
- package/src/formatting_service.ts +151 -0
- package/src/herb-language-server.ts +6 -0
- package/src/index.ts +10 -0
- package/src/linter_service.ts +63 -0
- package/src/parser_service.ts +59 -0
- package/src/project.ts +22 -0
- package/src/server.ts +111 -0
- package/src/service.ts +66 -0
- package/src/settings.ts +82 -0
- package/src/utils.ts +11 -0
- package/dist/herb-language-server +0 -20526
package/package.json
CHANGED
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/language-server",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Herb HTML+ERB Language Tools and Language Server Protocol integration.",
|
|
4
|
+
"version": "0.4.0",
|
|
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
|
-
"homepage": "https://herb-tools.dev",
|
|
17
17
|
"bin": {
|
|
18
|
-
"herb-language-server": "./
|
|
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",
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
19
32
|
},
|
|
20
33
|
"scripts": {
|
|
21
34
|
"clean": "rimraf dist",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"watch": "tsc -b -w",
|
|
26
|
-
"test": "echo 'TODO'",
|
|
35
|
+
"build": "yarn clean && tsc -b && rollup -c",
|
|
36
|
+
"dev": "tsc -b -w",
|
|
37
|
+
"test": "echo 'TODO: add tests'",
|
|
27
38
|
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
|
28
39
|
},
|
|
29
40
|
"files": [
|
|
30
|
-
"
|
|
41
|
+
"package.json",
|
|
42
|
+
"README.md",
|
|
43
|
+
"src/",
|
|
44
|
+
"bin/",
|
|
45
|
+
"dist/"
|
|
31
46
|
],
|
|
32
47
|
"dependencies": {
|
|
33
|
-
"@herb-tools/
|
|
48
|
+
"@herb-tools/formatter": "0.4.0",
|
|
49
|
+
"@herb-tools/linter": "0.4.0",
|
|
50
|
+
"@herb-tools/node-wasm": "0.4.0",
|
|
34
51
|
"dedent": "^1.6.0",
|
|
35
|
-
"typescript": "^5.8.3",
|
|
36
52
|
"vscode-languageserver": "^9.0.1",
|
|
37
53
|
"vscode-languageserver-textdocument": "^1.0.12"
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@rollup/plugin-commonjs": "^28.0.6",
|
|
41
|
-
"@rollup/plugin-typescript": "^12.1.3",
|
|
42
|
-
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
43
|
-
"@rollup/plugin-json": "^6.1.0",
|
|
44
|
-
"rimraf": "^6.0.1"
|
|
45
54
|
}
|
|
46
55
|
}
|
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
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type HerbConfigOptions = {
|
|
2
|
+
formatter?: {
|
|
3
|
+
enabled?: boolean
|
|
4
|
+
include?: string[]
|
|
5
|
+
exclude?: string[]
|
|
6
|
+
indentWidth?: number
|
|
7
|
+
maxLineLength?: number
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type HerbLSPConfig = {
|
|
12
|
+
version: string
|
|
13
|
+
createdAt: string
|
|
14
|
+
updatedAt: string
|
|
15
|
+
options: HerbConfigOptions
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
import path from "path"
|
|
19
|
+
import { version } from "../package.json"
|
|
20
|
+
import { promises as fs } from "fs"
|
|
21
|
+
|
|
22
|
+
export class Config {
|
|
23
|
+
static configPath = ".herb-lsp/config.json"
|
|
24
|
+
|
|
25
|
+
public readonly path: string
|
|
26
|
+
public config: HerbLSPConfig
|
|
27
|
+
|
|
28
|
+
constructor(projectPath: string, config: HerbLSPConfig) {
|
|
29
|
+
this.path = Config.configPathFromProjectPath(projectPath)
|
|
30
|
+
this.config = config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get version(): string {
|
|
34
|
+
return this.config.version
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get createdAt(): Date {
|
|
38
|
+
return new Date(this.config.createdAt)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get updatedAt(): Date {
|
|
42
|
+
return new Date(this.config.updatedAt)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get options(): HerbConfigOptions {
|
|
46
|
+
return this.config.options
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public toJSON() {
|
|
50
|
+
return JSON.stringify(this.config, null, " ")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private updateTimestamp() {
|
|
54
|
+
this.config.updatedAt = new Date().toISOString()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private updateVersion() {
|
|
58
|
+
this.config.version = version
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async write() {
|
|
62
|
+
this.updateVersion()
|
|
63
|
+
this.updateTimestamp()
|
|
64
|
+
|
|
65
|
+
const folder = path.dirname(this.path)
|
|
66
|
+
|
|
67
|
+
fs.stat(folder)
|
|
68
|
+
.then(() => {})
|
|
69
|
+
.catch(async () => await fs.mkdir(folder))
|
|
70
|
+
.finally(async () => await fs.writeFile(this.path, this.toJSON()))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async read() {
|
|
74
|
+
return await fs.readFile(this.path, "utf8")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static configPathFromProjectPath(projectPath: string) {
|
|
78
|
+
return path.join(projectPath, this.configPath)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static async fromPathOrNew(projectPath: string) {
|
|
82
|
+
try {
|
|
83
|
+
return await this.fromPath(projectPath)
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
return Config.newConfig(projectPath)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static async fromPath(projectPath: string) {
|
|
90
|
+
const configPath = Config.configPathFromProjectPath(projectPath)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const config = JSON.parse(await fs.readFile(configPath, "utf8"))
|
|
94
|
+
|
|
95
|
+
return new Config(projectPath, config)
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
throw new Error(`Error reading config file at: ${configPath}. Error: ${error.message}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static newConfig(projectPath: string): Config {
|
|
102
|
+
return new Config(projectPath, {
|
|
103
|
+
version,
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
options: {}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
2
|
+
import { Connection, Diagnostic } from "vscode-languageserver/node"
|
|
3
|
+
|
|
4
|
+
import { ParserService } from "./parser_service"
|
|
5
|
+
import { LinterService } from "./linter_service"
|
|
6
|
+
import { DocumentService } from "./document_service"
|
|
7
|
+
|
|
8
|
+
export class Diagnostics {
|
|
9
|
+
private readonly connection: Connection
|
|
10
|
+
private readonly documentService: DocumentService
|
|
11
|
+
private readonly parserService: ParserService
|
|
12
|
+
private readonly linterService: LinterService
|
|
13
|
+
private diagnostics: Map<TextDocument, Diagnostic[]> = new Map()
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
connection: Connection,
|
|
17
|
+
documentService: DocumentService,
|
|
18
|
+
parserService: ParserService,
|
|
19
|
+
linterService: LinterService,
|
|
20
|
+
) {
|
|
21
|
+
this.connection = connection
|
|
22
|
+
this.documentService = documentService
|
|
23
|
+
this.parserService = parserService
|
|
24
|
+
this.linterService = linterService
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async validate(textDocument: TextDocument) {
|
|
28
|
+
const parseResult = this.parserService.parseDocument(textDocument)
|
|
29
|
+
const lintResult = await this.linterService.lintDocument(parseResult.document, textDocument)
|
|
30
|
+
|
|
31
|
+
const allDiagnostics = [
|
|
32
|
+
...parseResult.diagnostics,
|
|
33
|
+
...lintResult.diagnostics,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
this.diagnostics.set(textDocument, allDiagnostics)
|
|
37
|
+
this.sendDiagnosticsFor(textDocument)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async refreshDocument(document: TextDocument) {
|
|
41
|
+
await this.validate(document)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async refreshAllDocuments() {
|
|
45
|
+
const documents = this.documentService.getAll()
|
|
46
|
+
await Promise.all(documents.map(document => this.refreshDocument(document)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private sendDiagnosticsFor(textDocument: TextDocument) {
|
|
50
|
+
const diagnostics = this.diagnostics.get(textDocument) || []
|
|
51
|
+
|
|
52
|
+
this.connection.sendDiagnostics({
|
|
53
|
+
uri: textDocument.uri,
|
|
54
|
+
diagnostics,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
this.diagnostics.delete(textDocument)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Connection, TextDocuments } from "vscode-languageserver/node"
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
|
+
|
|
4
|
+
export class DocumentService {
|
|
5
|
+
public documents: TextDocuments<TextDocument>
|
|
6
|
+
document?: TextDocument
|
|
7
|
+
|
|
8
|
+
constructor(connection: Connection) {
|
|
9
|
+
this.documents = new TextDocuments(TextDocument)
|
|
10
|
+
|
|
11
|
+
// Make the text document manager listen on the connection
|
|
12
|
+
// for open, change and close text document events
|
|
13
|
+
this.documents.listen(connection)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(uri: string) {
|
|
17
|
+
return this.documents.get(uri)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getAll() {
|
|
21
|
+
return this.documents.all()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get onDidChangeContent() {
|
|
25
|
+
return this.documents.onDidChangeContent
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get onDidOpen() {
|
|
29
|
+
return this.documents.onDidOpen
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get onDidClose() {
|
|
33
|
+
return this.documents.onDidClose
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
// Get VS Code settings
|
|
87
|
+
const settings = await this.settings.getDocumentSettings(uri)
|
|
88
|
+
|
|
89
|
+
// Get project config options
|
|
90
|
+
const projectFormatter = this.config?.options.formatter || {}
|
|
91
|
+
|
|
92
|
+
// Merge options with precedence: project config > VS Code settings > defaults
|
|
93
|
+
return {
|
|
94
|
+
indentWidth: projectFormatter.indentWidth ?? settings.formatter?.indentWidth ?? defaultFormatOptions.indentWidth,
|
|
95
|
+
maxLineLength: projectFormatter.maxLineLength ?? settings.formatter?.maxLineLength ?? defaultFormatOptions.maxLineLength
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async performFormatting(params: DocumentFormattingParams): Promise<TextEdit[]> {
|
|
100
|
+
const document = this.documents.get(params.textDocument.uri)
|
|
101
|
+
|
|
102
|
+
if (!document) {
|
|
103
|
+
return []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const options = await this.getFormatterOptions(params.textDocument.uri)
|
|
108
|
+
const formatter = new Formatter(this.project.herbBackend, options)
|
|
109
|
+
|
|
110
|
+
const text = document.getText()
|
|
111
|
+
const newText = formatter.format(text)
|
|
112
|
+
|
|
113
|
+
if (newText === text) {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const range: Range = {
|
|
118
|
+
start: Position.create(0, 0),
|
|
119
|
+
end: Position.create(document.lineCount, 0)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [{ range, newText }]
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.connection.console.error(`Formatting failed: ${error}`)
|
|
125
|
+
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async formatDocument(params: DocumentFormattingParams): Promise<TextEdit[]> {
|
|
131
|
+
// Check VS Code settings first
|
|
132
|
+
const settings = await this.settings.getDocumentSettings(params.textDocument.uri)
|
|
133
|
+
|
|
134
|
+
if (settings.formatter?.enabled === false) {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check project config and file patterns
|
|
139
|
+
const filePath = params.textDocument.uri.replace(/^file:\/\//, '')
|
|
140
|
+
|
|
141
|
+
if (!(await this.shouldFormatFile(filePath))) {
|
|
142
|
+
return []
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.performFormatting(params)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async formatDocumentIgnoreConfig(params: DocumentFormattingParams): Promise<TextEdit[]> {
|
|
149
|
+
return this.performFormatting(params)
|
|
150
|
+
}
|
|
151
|
+
}
|
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
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Herb, HerbBackend } from "@herb-tools/node-wasm"
|
|
2
|
+
import { Connection } from "vscode-languageserver/node"
|
|
3
|
+
|
|
4
|
+
export class Project {
|
|
5
|
+
connection: Connection
|
|
6
|
+
projectPath: string
|
|
7
|
+
herbBackend: HerbBackend
|
|
8
|
+
|
|
9
|
+
constructor(connection: Connection, projectPath: string) {
|
|
10
|
+
this.projectPath = projectPath
|
|
11
|
+
this.connection = connection
|
|
12
|
+
this.herbBackend = Herb
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async initialize() {
|
|
16
|
+
await this.herbBackend.load()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async refresh() {
|
|
20
|
+
// TODO
|
|
21
|
+
}
|
|
22
|
+
}
|