@sailshq/language-server 0.5.2 → 0.6.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.
- 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 +77 -3
- 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')
|
|
@@ -36,6 +42,17 @@ const documents = new lsp.TextDocuments(TextDocument)
|
|
|
36
42
|
const sailsParser = new SailsParser()
|
|
37
43
|
let typeMap
|
|
38
44
|
|
|
45
|
+
// Helper to check if a document is within the current project
|
|
46
|
+
function isInProject(documentUri) {
|
|
47
|
+
if (!sailsParser.rootDir) return false
|
|
48
|
+
try {
|
|
49
|
+
const documentPath = decodeURIComponent(new URL(documentUri).pathname)
|
|
50
|
+
return documentPath.startsWith(sailsParser.rootDir)
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
connection.onInitialize(async (params) => {
|
|
40
57
|
const rootPath = params.workspaceFolders?.[0]?.uri
|
|
41
58
|
? new URL(params.workspaceFolders[0].uri).pathname
|
|
@@ -50,18 +67,60 @@ connection.onInitialize(async (params) => {
|
|
|
50
67
|
definitionProvider: true,
|
|
51
68
|
completionProvider: {
|
|
52
69
|
triggerCharacters: ['"', "'", '.', '{', ',', ' ', '\n']
|
|
70
|
+
},
|
|
71
|
+
codeActionProvider: {
|
|
72
|
+
codeActionKinds: [lsp.CodeActionKind.QuickFix]
|
|
73
|
+
},
|
|
74
|
+
executeCommandProvider: {
|
|
75
|
+
commands: codeActions.getCommands()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
connection.onInitialized(() => {
|
|
82
|
+
// Register for file create/delete notifications in api/ and config/ directories
|
|
83
|
+
connection.client.register(lsp.DidChangeWatchedFilesNotification.type, {
|
|
84
|
+
watchers: [
|
|
85
|
+
{ globPattern: '**/api/**/*.js' },
|
|
86
|
+
{ globPattern: '**/api/**/*.ejs' },
|
|
87
|
+
{ globPattern: '**/config/**/*.js' },
|
|
88
|
+
{ globPattern: '**/views/**/*.ejs' },
|
|
89
|
+
{ globPattern: '**/assets/js/pages/**/*.{vue,js,ts,jsx,tsx,svelte,html}' }
|
|
90
|
+
]
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
connection.onDidChangeWatchedFiles(async (params) => {
|
|
95
|
+
// Check if any relevant files were created or deleted
|
|
96
|
+
const hasRelevantChange = params.changes.some(
|
|
97
|
+
(change) =>
|
|
98
|
+
change.type === lsp.FileChangeType.Created ||
|
|
99
|
+
change.type === lsp.FileChangeType.Deleted
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (hasRelevantChange) {
|
|
103
|
+
typeMap = await sailsParser.buildTypeMap()
|
|
104
|
+
connection.console.log('Type map updated due to file create/delete.')
|
|
105
|
+
|
|
106
|
+
// Re-validate all open documents in the current project
|
|
107
|
+
for (const document of documents.all()) {
|
|
108
|
+
if (isInProject(document.uri)) {
|
|
109
|
+
validateDocument(connection, document, typeMap)
|
|
53
110
|
}
|
|
54
111
|
}
|
|
55
112
|
}
|
|
56
113
|
})
|
|
57
114
|
|
|
58
115
|
documents.onDidOpen((open) => {
|
|
59
|
-
if (typeMap) {
|
|
116
|
+
if (typeMap && isInProject(open.document.uri)) {
|
|
60
117
|
validateDocument(connection, open.document, typeMap)
|
|
61
118
|
}
|
|
62
119
|
})
|
|
63
120
|
|
|
64
121
|
documents.onDidChangeContent(async (change) => {
|
|
122
|
+
if (!isInProject(change.document.uri)) return
|
|
123
|
+
|
|
65
124
|
const documentUri = change.document.uri
|
|
66
125
|
if (documentUri.includes('api/') || documentUri.includes('config')) {
|
|
67
126
|
typeMap = await sailsParser.buildTypeMap()
|
|
@@ -75,7 +134,7 @@ documents.onDidChangeContent(async (change) => {
|
|
|
75
134
|
|
|
76
135
|
connection.onDefinition(async (params) => {
|
|
77
136
|
const document = documents.get(params.textDocument.uri)
|
|
78
|
-
if (!document) {
|
|
137
|
+
if (!document || !isInProject(params.textDocument.uri)) {
|
|
79
138
|
return null
|
|
80
139
|
}
|
|
81
140
|
|
|
@@ -114,7 +173,7 @@ connection.onDefinition(async (params) => {
|
|
|
114
173
|
|
|
115
174
|
connection.onCompletion(async (params) => {
|
|
116
175
|
const document = documents.get(params.textDocument.uri)
|
|
117
|
-
if (!document) {
|
|
176
|
+
if (!document || !isInProject(params.textDocument.uri)) {
|
|
118
177
|
return null
|
|
119
178
|
}
|
|
120
179
|
const [
|
|
@@ -170,6 +229,21 @@ connection.onCompletion(async (params) => {
|
|
|
170
229
|
return null
|
|
171
230
|
})
|
|
172
231
|
|
|
232
|
+
connection.onCodeAction((params) => {
|
|
233
|
+
if (!isInProject(params.textDocument.uri)) {
|
|
234
|
+
return []
|
|
235
|
+
}
|
|
236
|
+
return codeActions.getCodeActions(params)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
connection.onExecuteCommand(async (params) => {
|
|
240
|
+
await codeActions.executeCommand(params, {
|
|
241
|
+
rootDir: sailsParser.rootDir,
|
|
242
|
+
execAsync,
|
|
243
|
+
connection
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
173
247
|
documents.listen(connection)
|
|
174
248
|
connection.listen()
|
|
175
249
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sailshq/language-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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
|