@sailshq/language-server 0.5.1 → 0.6.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/SailsParser.js +4 -1
- package/code-actions/create-generator.js +65 -0
- package/code-actions/generate-action.js +8 -0
- package/code-actions/generate-adapter.js +8 -0
- package/code-actions/generate-helper.js +8 -0
- package/code-actions/generate-hook.js +8 -0
- package/code-actions/generate-model.js +8 -0
- package/code-actions/generate-response.js +8 -0
- package/code-actions/index.js +33 -0
- package/index.js +54 -0
- package/package.json +1 -4
- package/validators/validate-action-exist.js +16 -10
- package/validators/validate-data-type.js +7 -0
- package/validators/validate-helper-input-exist.js +7 -0
- package/validators/validate-model-attribute-exist.js +7 -0
- package/validators/validate-model-exist.js +29 -22
- package/validators/validate-page-exist.js +6 -0
- package/validators/validate-required-helper-input.js +7 -0
- package/validators/validate-required-model-attribute.js +7 -0
package/SailsParser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fsSync = require('fs')
|
|
1
2
|
const fs = require('fs').promises
|
|
2
3
|
const path = require('path')
|
|
3
4
|
const acorn = require('acorn')
|
|
@@ -110,12 +111,14 @@ class SailsParser {
|
|
|
110
111
|
for (const { routePattern, actionName } of actionsToParse) {
|
|
111
112
|
const filePath =
|
|
112
113
|
path.join(actionsRoot, ...actionName.split('/')) + '.js'
|
|
113
|
-
const
|
|
114
|
+
const exists = fsSync.existsSync(filePath)
|
|
115
|
+
const actionInfo = exists ? await this.#parseAction(filePath) : {}
|
|
114
116
|
|
|
115
117
|
routes[routePattern] = {
|
|
116
118
|
action: {
|
|
117
119
|
name: actionName,
|
|
118
120
|
path: filePath,
|
|
121
|
+
exists,
|
|
119
122
|
...actionInfo
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
const COMMAND_TIMEOUT = 30000
|
|
4
|
+
|
|
5
|
+
module.exports = function createGenerator({
|
|
6
|
+
type,
|
|
7
|
+
diagnosticCode,
|
|
8
|
+
dataKey,
|
|
9
|
+
validationRegex
|
|
10
|
+
}) {
|
|
11
|
+
function isValid(name) {
|
|
12
|
+
return name && validationRegex.test(name)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
diagnosticCode,
|
|
17
|
+
command: `sails.generate${type.charAt(0).toUpperCase() + type.slice(1)}`,
|
|
18
|
+
|
|
19
|
+
createCodeAction(diagnostic) {
|
|
20
|
+
const name = diagnostic.data?.[dataKey]
|
|
21
|
+
if (!isValid(name)) return null
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
title: `Generate ${type} '${name}'`,
|
|
25
|
+
kind: lsp.CodeActionKind.QuickFix,
|
|
26
|
+
diagnostics: [diagnostic],
|
|
27
|
+
isPreferred: true,
|
|
28
|
+
command: {
|
|
29
|
+
title: `Generate ${type} '${name}'`,
|
|
30
|
+
command: this.command,
|
|
31
|
+
arguments: [name]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async executeCommand(args, { rootDir, execAsync, connection }) {
|
|
37
|
+
const name = args[0]
|
|
38
|
+
if (!isValid(name)) return
|
|
39
|
+
|
|
40
|
+
if (!rootDir) {
|
|
41
|
+
connection.window.showErrorMessage(
|
|
42
|
+
`Cannot generate ${type}: workspace root not found.`
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
connection.window.showInformationMessage(
|
|
49
|
+
`Generating ${type} '${name}'...`
|
|
50
|
+
)
|
|
51
|
+
await execAsync(`npx sails generate ${type} ${name}`, {
|
|
52
|
+
cwd: rootDir,
|
|
53
|
+
timeout: COMMAND_TIMEOUT
|
|
54
|
+
})
|
|
55
|
+
connection.window.showInformationMessage(
|
|
56
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} '${name}' generated successfully.`
|
|
57
|
+
)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
connection.window.showErrorMessage(
|
|
60
|
+
`Failed to generate ${type}: ${error.message}`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const handlers = [
|
|
2
|
+
require('./generate-action'),
|
|
3
|
+
require('./generate-model'),
|
|
4
|
+
require('./generate-helper'),
|
|
5
|
+
require('./generate-hook'),
|
|
6
|
+
require('./generate-response'),
|
|
7
|
+
require('./generate-adapter')
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
getCommands() {
|
|
12
|
+
return handlers.map((h) => h.command)
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
getCodeActions(params) {
|
|
16
|
+
const actions = []
|
|
17
|
+
for (const diagnostic of params.context.diagnostics) {
|
|
18
|
+
const handler = handlers.find((h) => h.diagnosticCode === diagnostic.code)
|
|
19
|
+
if (handler) {
|
|
20
|
+
const action = handler.createCodeAction(diagnostic)
|
|
21
|
+
if (action) actions.push(action)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return actions
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async executeCommand(params, context) {
|
|
28
|
+
const handler = handlers.find((h) => h.command === params.command)
|
|
29
|
+
if (handler) {
|
|
30
|
+
await handler.executeCommand(params.arguments, context)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
const lsp = require('vscode-languageserver/node')
|
|
2
2
|
const TextDocument = require('vscode-languageserver-textdocument').TextDocument
|
|
3
|
+
const { exec } = require('child_process')
|
|
4
|
+
const { promisify } = require('util')
|
|
5
|
+
const execAsync = promisify(exec)
|
|
3
6
|
const SailsParser = require('./SailsParser')
|
|
4
7
|
|
|
5
8
|
// Validators
|
|
6
9
|
const validateDocument = require('./validators/validate-document')
|
|
7
10
|
|
|
11
|
+
// Code Actions
|
|
12
|
+
const codeActions = require('./code-actions')
|
|
13
|
+
|
|
8
14
|
// Go-to definitions
|
|
9
15
|
const goToAction = require('./go-to-definitions/go-to-action')
|
|
10
16
|
const goToView = require('./go-to-definitions/go-to-view')
|
|
@@ -50,11 +56,49 @@ connection.onInitialize(async (params) => {
|
|
|
50
56
|
definitionProvider: true,
|
|
51
57
|
completionProvider: {
|
|
52
58
|
triggerCharacters: ['"', "'", '.', '{', ',', ' ', '\n']
|
|
59
|
+
},
|
|
60
|
+
codeActionProvider: {
|
|
61
|
+
codeActionKinds: [lsp.CodeActionKind.QuickFix]
|
|
62
|
+
},
|
|
63
|
+
executeCommandProvider: {
|
|
64
|
+
commands: codeActions.getCommands()
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
68
|
})
|
|
57
69
|
|
|
70
|
+
connection.onInitialized(() => {
|
|
71
|
+
// Register for file create/delete notifications in api/ and config/ directories
|
|
72
|
+
connection.client.register(lsp.DidChangeWatchedFilesNotification.type, {
|
|
73
|
+
watchers: [
|
|
74
|
+
{ globPattern: '**/api/**/*.js' },
|
|
75
|
+
{ globPattern: '**/api/**/*.ejs' },
|
|
76
|
+
{ globPattern: '**/config/**/*.js' },
|
|
77
|
+
{ globPattern: '**/views/**/*.ejs' },
|
|
78
|
+
{ globPattern: '**/assets/js/pages/**/*.{vue,js,ts,jsx,tsx,svelte,html}' }
|
|
79
|
+
]
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
connection.onDidChangeWatchedFiles(async (params) => {
|
|
84
|
+
// Check if any relevant files were created or deleted
|
|
85
|
+
const hasRelevantChange = params.changes.some(
|
|
86
|
+
(change) =>
|
|
87
|
+
change.type === lsp.FileChangeType.Created ||
|
|
88
|
+
change.type === lsp.FileChangeType.Deleted
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if (hasRelevantChange) {
|
|
92
|
+
typeMap = await sailsParser.buildTypeMap()
|
|
93
|
+
connection.console.log('Type map updated due to file create/delete.')
|
|
94
|
+
|
|
95
|
+
// Re-validate all open documents
|
|
96
|
+
for (const document of documents.all()) {
|
|
97
|
+
validateDocument(connection, document, typeMap)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
58
102
|
documents.onDidOpen((open) => {
|
|
59
103
|
if (typeMap) {
|
|
60
104
|
validateDocument(connection, open.document, typeMap)
|
|
@@ -170,6 +214,16 @@ connection.onCompletion(async (params) => {
|
|
|
170
214
|
return null
|
|
171
215
|
})
|
|
172
216
|
|
|
217
|
+
connection.onCodeAction((params) => codeActions.getCodeActions(params))
|
|
218
|
+
|
|
219
|
+
connection.onExecuteCommand(async (params) => {
|
|
220
|
+
await codeActions.executeCommand(params, {
|
|
221
|
+
rootDir: sailsParser.rootDir,
|
|
222
|
+
execAsync,
|
|
223
|
+
connection
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
173
227
|
documents.listen(connection)
|
|
174
228
|
connection.listen()
|
|
175
229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailshq/language-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Language Server Protocol server for Sails Language Service",
|
|
5
5
|
"homepage": "https://sailjs.com",
|
|
6
6
|
"repository": {
|
|
@@ -27,8 +27,5 @@
|
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@sailshq/language-server": "^0.2.1"
|
|
33
30
|
}
|
|
34
31
|
}
|
|
@@ -5,24 +5,30 @@ const walk = require('acorn-walk')
|
|
|
5
5
|
module.exports = function validateActionExist(document, typeMap) {
|
|
6
6
|
const diagnostics = []
|
|
7
7
|
|
|
8
|
-
if (!document.uri.endsWith('routes.js')) return diagnostics
|
|
8
|
+
if (!document.uri.endsWith('config/routes.js')) return diagnostics
|
|
9
|
+
|
|
9
10
|
const actions = extractActionInfo(document)
|
|
10
11
|
|
|
11
12
|
for (const { action, range } of actions) {
|
|
12
13
|
if (isUrlOrRedirect(action)) continue
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
// Find any route entry that references this action and check if it exists
|
|
16
|
+
const routeEntry = Object.values(typeMap.routes || {}).find(
|
|
14
17
|
(route) => route.action?.name === action
|
|
15
18
|
)
|
|
16
19
|
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Action exists if we found a route entry with exists: true
|
|
21
|
+
// If no route entry found, the action was just added and typeMap is stale,
|
|
22
|
+
// so we need to check exists flag which was set when typeMap was built
|
|
23
|
+
if (routeEntry && routeEntry.action?.exists === false) {
|
|
24
|
+
const diagnostic = lsp.Diagnostic.create(
|
|
25
|
+
range,
|
|
26
|
+
`'${action}' action does not exist. Please check the name or create it.`,
|
|
27
|
+
lsp.DiagnosticSeverity.Error,
|
|
28
|
+
'action-not-found'
|
|
25
29
|
)
|
|
30
|
+
diagnostic.data = { actionName: action }
|
|
31
|
+
diagnostics.push(diagnostic)
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
return diagnostics
|
|
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
|
|
|
4
4
|
|
|
5
5
|
module.exports = function validateDataType(document, typeMap) {
|
|
6
6
|
const diagnostics = []
|
|
7
|
+
const documentUri = document.uri
|
|
8
|
+
|
|
9
|
+
// Only validate files in backend directories where data types are relevant
|
|
10
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
11
|
+
return diagnostics
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const text = document.getText()
|
|
8
15
|
|
|
9
16
|
try {
|
|
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
|
|
|
4
4
|
|
|
5
5
|
module.exports = function validateHelperInputExist(document, typeMap) {
|
|
6
6
|
const diagnostics = []
|
|
7
|
+
const documentUri = document.uri
|
|
8
|
+
|
|
9
|
+
// Only validate files in backend directories where helpers are accessible
|
|
10
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
11
|
+
return diagnostics
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const text = document.getText()
|
|
8
15
|
|
|
9
16
|
try {
|
|
@@ -130,6 +130,13 @@ function validateCriteriaAttributes(
|
|
|
130
130
|
*/
|
|
131
131
|
module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
132
132
|
const diagnostics = []
|
|
133
|
+
const documentUri = document.uri
|
|
134
|
+
|
|
135
|
+
// Only validate files in backend directories where models are accessible
|
|
136
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
137
|
+
return diagnostics
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
const text = document.getText()
|
|
134
141
|
|
|
135
142
|
// Build a lowercased model map for robust case-insensitive lookup
|
|
@@ -7,6 +7,13 @@ const lsp = require('vscode-languageserver/node')
|
|
|
7
7
|
*/
|
|
8
8
|
module.exports = function validateModelExist(document, typeMap) {
|
|
9
9
|
const diagnostics = []
|
|
10
|
+
const documentUri = document.uri
|
|
11
|
+
|
|
12
|
+
// Only validate files in backend directories where models are accessible
|
|
13
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
14
|
+
return diagnostics
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
const text = document.getText()
|
|
11
18
|
const models = typeMap.models || {}
|
|
12
19
|
const lowercasedModelMap = {}
|
|
@@ -42,17 +49,17 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
42
49
|
continue
|
|
43
50
|
}
|
|
44
51
|
if (!modelExists(modelName)) {
|
|
45
|
-
|
|
46
|
-
lsp.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
'sails-lsp'
|
|
54
|
-
)
|
|
52
|
+
const diagnostic = lsp.Diagnostic.create(
|
|
53
|
+
lsp.Range.create(
|
|
54
|
+
document.positionAt(match.index),
|
|
55
|
+
document.positionAt(match.index + modelName.length)
|
|
56
|
+
),
|
|
57
|
+
`Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
|
|
58
|
+
lsp.DiagnosticSeverity.Error,
|
|
59
|
+
'model-not-found'
|
|
55
60
|
)
|
|
61
|
+
diagnostic.data = { modelName }
|
|
62
|
+
diagnostics.push(diagnostic)
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
// sails.models.user.find() or sails.models.User.find()
|
|
@@ -61,19 +68,19 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
61
68
|
while ((match = sailsModelCallRegex.exec(text)) !== null) {
|
|
62
69
|
const modelName = match[1]
|
|
63
70
|
if (!modelExistsLowercased(modelName)) {
|
|
64
|
-
|
|
65
|
-
lsp.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'sails-lsp'
|
|
75
|
-
)
|
|
71
|
+
const diagnostic = lsp.Diagnostic.create(
|
|
72
|
+
lsp.Range.create(
|
|
73
|
+
document.positionAt(match.index + 'sails.models.'.length),
|
|
74
|
+
document.positionAt(
|
|
75
|
+
match.index + 'sails.models.'.length + modelName.length
|
|
76
|
+
)
|
|
77
|
+
),
|
|
78
|
+
`Model '${modelName}' does not exist in this Sails project.`,
|
|
79
|
+
lsp.DiagnosticSeverity.Error,
|
|
80
|
+
'model-not-found'
|
|
76
81
|
)
|
|
82
|
+
diagnostic.data = { modelName }
|
|
83
|
+
diagnostics.push(diagnostic)
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
return diagnostics
|
|
@@ -2,6 +2,12 @@ const lsp = require('vscode-languageserver/node')
|
|
|
2
2
|
|
|
3
3
|
module.exports = function validatePageExist(document, typeMap) {
|
|
4
4
|
const diagnostics = []
|
|
5
|
+
const documentUri = document.uri
|
|
6
|
+
|
|
7
|
+
// Only validate files in api/ where Inertia pages are referenced
|
|
8
|
+
if (!documentUri.includes('/api/')) {
|
|
9
|
+
return diagnostics
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
const pages = extractPageReferences(document)
|
|
7
13
|
for (const { page, range } of pages) {
|
|
@@ -4,6 +4,13 @@ const walk = require('acorn-walk')
|
|
|
4
4
|
|
|
5
5
|
module.exports = function validateRequiredHelperInput(document, typeMap) {
|
|
6
6
|
const diagnostics = []
|
|
7
|
+
const documentUri = document.uri
|
|
8
|
+
|
|
9
|
+
// Only validate files in backend directories where helpers are accessible
|
|
10
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
11
|
+
return diagnostics
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const text = document.getText()
|
|
8
15
|
|
|
9
16
|
try {
|
|
@@ -2,6 +2,13 @@ const lsp = require('vscode-languageserver/node')
|
|
|
2
2
|
|
|
3
3
|
module.exports = function validateRequiredModelAttribute(document, typeMap) {
|
|
4
4
|
const diagnostics = []
|
|
5
|
+
const documentUri = document.uri
|
|
6
|
+
|
|
7
|
+
// Only validate files in backend directories where models are accessible
|
|
8
|
+
if (!documentUri.includes('/api/') && !documentUri.includes('/scripts/')) {
|
|
9
|
+
return diagnostics
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
const text = document.getText()
|
|
6
13
|
const models = typeMap.models || {}
|
|
7
14
|
|