@sailshq/language-server 0.5.2 → 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-model-exist.js +22 -22
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
|
|
@@ -49,17 +49,17 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
49
49
|
continue
|
|
50
50
|
}
|
|
51
51
|
if (!modelExists(modelName)) {
|
|
52
|
-
|
|
53
|
-
lsp.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'sails-lsp'
|
|
61
|
-
)
|
|
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'
|
|
62
60
|
)
|
|
61
|
+
diagnostic.data = { modelName }
|
|
62
|
+
diagnostics.push(diagnostic)
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
// sails.models.user.find() or sails.models.User.find()
|
|
@@ -68,19 +68,19 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
68
68
|
while ((match = sailsModelCallRegex.exec(text)) !== null) {
|
|
69
69
|
const modelName = match[1]
|
|
70
70
|
if (!modelExistsLowercased(modelName)) {
|
|
71
|
-
|
|
72
|
-
lsp.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'sails-lsp'
|
|
82
|
-
)
|
|
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'
|
|
83
81
|
)
|
|
82
|
+
diagnostic.data = { modelName }
|
|
83
|
+
diagnostics.push(diagnostic)
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
return diagnostics
|