@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 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 actionInfo = await this.#parseAction(filePath)
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,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'action',
5
+ diagnosticCode: 'action-not-found',
6
+ dataKey: 'actionName',
7
+ validationRegex: /^[a-zA-Z0-9/_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'adapter',
5
+ diagnosticCode: null,
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'helper',
5
+ diagnosticCode: null, // No diagnostic triggers this - manual command only
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9/_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'hook',
5
+ diagnosticCode: null, // No diagnostic triggers this - manual command only
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'model',
5
+ diagnosticCode: 'model-not-found',
6
+ dataKey: 'modelName',
7
+ validationRegex: /^[A-Za-z0-9_]+$/
8
+ })
@@ -0,0 +1,8 @@
1
+ const createGenerator = require('./create-generator')
2
+
3
+ module.exports = createGenerator({
4
+ type: 'response',
5
+ diagnosticCode: null,
6
+ dataKey: null,
7
+ validationRegex: /^[a-zA-Z0-9_-]+$/
8
+ })
@@ -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.5.2",
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
- const routeExists = Object.values(typeMap.routes || {}).some(
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 (!routeExists) {
18
- diagnostics.push(
19
- lsp.Diagnostic.create(
20
- range,
21
- `'${action}' action does not exist. Please check the name or create it.`,
22
- lsp.DiagnosticSeverity.Error,
23
- 'sails-lsp'
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
- diagnostics.push(
53
- lsp.Diagnostic.create(
54
- lsp.Range.create(
55
- document.positionAt(match.index),
56
- document.positionAt(match.index + modelName.length)
57
- ),
58
- `Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
59
- lsp.DiagnosticSeverity.Error,
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
- diagnostics.push(
72
- lsp.Diagnostic.create(
73
- lsp.Range.create(
74
- document.positionAt(match.index + 'sails.models.'.length),
75
- document.positionAt(
76
- match.index + 'sails.models.'.length + modelName.length
77
- )
78
- ),
79
- `Model '${modelName}' does not exist in this Sails project.`,
80
- lsp.DiagnosticSeverity.Error,
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