@sailshq/language-server 0.0.5 → 0.2.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.
Files changed (38) hide show
  1. package/SailsParser.js +652 -0
  2. package/completions/actions-completion.js +36 -0
  3. package/completions/data-types-completion.js +39 -0
  4. package/completions/helper-inputs-completion.js +91 -0
  5. package/completions/helpers-completion.js +85 -0
  6. package/completions/inertia-pages-completion.js +33 -0
  7. package/completions/input-props-completion.js +52 -0
  8. package/completions/model-attribute-props-completion.js +57 -0
  9. package/completions/model-attributes-completion.js +195 -0
  10. package/completions/model-methods-completion.js +71 -0
  11. package/completions/models-completion.js +52 -0
  12. package/completions/policies-completion.js +32 -0
  13. package/completions/views-completion.js +35 -0
  14. package/go-to-definitions/go-to-action.js +26 -49
  15. package/go-to-definitions/go-to-helper.js +37 -45
  16. package/go-to-definitions/go-to-model.js +39 -0
  17. package/go-to-definitions/go-to-page.js +38 -0
  18. package/go-to-definitions/go-to-policy.js +23 -72
  19. package/go-to-definitions/go-to-view.js +28 -55
  20. package/index.js +103 -19
  21. package/package.json +1 -1
  22. package/validators/validate-action-exist.js +28 -51
  23. package/validators/validate-data-type.js +34 -0
  24. package/validators/validate-document.js +42 -4
  25. package/validators/validate-helper-input-exist.js +42 -0
  26. package/validators/validate-model-attribute-exist.js +297 -0
  27. package/validators/validate-model-exist.js +64 -0
  28. package/validators/validate-page-exist.js +42 -0
  29. package/validators/validate-policy-exist.js +45 -0
  30. package/validators/validate-required-helper-input.js +49 -0
  31. package/validators/validate-required-model-attribute.js +56 -0
  32. package/validators/validate-view-exist.js +86 -0
  33. package/completions/sails-completions.js +0 -63
  34. package/go-to-definitions/go-to-inertia-page.js +0 -53
  35. package/helpers/find-fn-line.js +0 -21
  36. package/helpers/find-project-root.js +0 -18
  37. package/helpers/find-sails.js +0 -12
  38. package/helpers/load-sails.js +0 -39
package/index.js CHANGED
@@ -1,40 +1,74 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const TextDocument = require('vscode-languageserver-textdocument').TextDocument
3
+ const SailsParser = require('./SailsParser')
3
4
 
4
5
  // Validators
5
6
  const validateDocument = require('./validators/validate-document')
6
7
 
7
8
  // Go-to definitions
8
9
  const goToAction = require('./go-to-definitions/go-to-action')
9
- const goToPolicy = require('./go-to-definitions/go-to-policy')
10
10
  const goToView = require('./go-to-definitions/go-to-view')
11
- const goToInertiaPage = require('./go-to-definitions/go-to-inertia-page')
11
+ const goToPage = require('./go-to-definitions/go-to-page')
12
+ const goToPolicy = require('./go-to-definitions/go-to-policy')
12
13
  const goToHelper = require('./go-to-definitions/go-to-helper')
14
+ const goToModel = require('./go-to-definitions/go-to-model')
13
15
 
14
16
  // Completions
15
- const sailsCompletions = require('./completions/sails-completions')
17
+ const actionsCompletion = require('./completions/actions-completion')
18
+ const dataTypesCompletion = require('./completions/data-types-completion')
19
+ const modelAttributePropsCompletion = require('./completions/model-attribute-props-completion')
20
+ const inputPropsCompletion = require('./completions/input-props-completion')
21
+ const inertiaPagesCompletion = require('./completions/inertia-pages-completion')
22
+ const modelsCompletion = require('./completions/models-completion')
23
+ const policiesCompletion = require('./completions/policies-completion')
24
+ const viewsCompletion = require('./completions/views-completion')
25
+ const modelMethodsCompletion = require('./completions/model-methods-completion')
26
+ const modelAttributesCompletion = require('./completions/model-attributes-completion')
27
+ const helperInputsCompletion = require('./completions/helper-inputs-completion')
28
+ const helpersCompletion = require('./completions/helpers-completion')
16
29
 
17
30
  const connection = lsp.createConnection(lsp.ProposedFeatures.all)
18
31
  const documents = new lsp.TextDocuments(TextDocument)
19
32
 
20
- connection.onInitialize((params) => {
33
+ // Create a new SailsParser instance
34
+ const sailsParser = new SailsParser()
35
+ let typeMap
36
+
37
+ connection.onInitialize(async (params) => {
38
+ const rootPath = params.workspaceFolders?.[0]?.uri
39
+ ? new URL(params.workspaceFolders[0].uri).pathname
40
+ : undefined
41
+
42
+ sailsParser.setRootDir(rootPath)
43
+ typeMap = await sailsParser.buildTypeMap()
44
+
21
45
  return {
22
46
  capabilities: {
23
47
  textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
24
48
  definitionProvider: true,
25
49
  completionProvider: {
26
- triggerCharacters: ['"', "'", '.']
50
+ triggerCharacters: ['"', "'", '.', '{', ',', ' ', '\n']
27
51
  }
28
52
  }
29
53
  }
30
54
  })
31
55
 
32
56
  documents.onDidOpen((open) => {
33
- validateDocument(connection, open.document)
57
+ if (typeMap) {
58
+ validateDocument(connection, open.document, typeMap)
59
+ }
34
60
  })
35
61
 
36
- documents.onDidChangeContent((change) => {
37
- validateDocument(connection, change.document)
62
+ documents.onDidChangeContent(async (change) => {
63
+ const documentUri = change.document.uri
64
+ if (documentUri.includes('api/') || documentUri.includes('config')) {
65
+ typeMap = await sailsParser.buildTypeMap()
66
+ connection.console.log('Type map updated due to file change.')
67
+ }
68
+
69
+ if (typeMap) {
70
+ validateDocument(connection, change.document, typeMap)
71
+ }
38
72
  })
39
73
 
40
74
  connection.onDefinition(async (params) => {
@@ -43,20 +77,30 @@ connection.onDefinition(async (params) => {
43
77
  return null
44
78
  }
45
79
 
46
- const actionDefinition = await goToAction(document, params.position)
47
- const policyDefinition = await goToPolicy(document, params.position)
48
- const viewDefinition = await goToView(document, params.position)
49
- const inertiaPageDefinition = await goToInertiaPage(document, params.position)
50
- const helperDefinition = await goToHelper(document, params.position)
80
+ const [
81
+ actionDefinition,
82
+ viewDefinition,
83
+ pageDefinition,
84
+ policyDefinition,
85
+ helperDefinition,
86
+ modelDefinition
87
+ ] = await Promise.all([
88
+ goToAction(document, params.position, typeMap),
89
+ goToView(document, params.position, typeMap),
90
+ goToPage(document, params.position, typeMap),
91
+ goToPolicy(document, params.position, typeMap),
92
+ goToHelper(document, params.position, typeMap),
93
+ goToModel(document, params.position, typeMap)
94
+ ])
51
95
 
52
96
  const definitions = [
53
97
  actionDefinition,
54
- policyDefinition,
55
98
  viewDefinition,
56
- inertiaPageDefinition,
57
- helperDefinition
99
+ pageDefinition,
100
+ policyDefinition,
101
+ helperDefinition,
102
+ modelDefinition
58
103
  ].filter(Boolean)
59
-
60
104
  return definitions.length > 0 ? definitions : null
61
105
  })
62
106
 
@@ -65,8 +109,48 @@ connection.onCompletion(async (params) => {
65
109
  if (!document) {
66
110
  return null
67
111
  }
68
-
69
- const completions = await sailsCompletions(document, params.position)
112
+ const [
113
+ actionCompletion,
114
+ dataTypeCompletion,
115
+ modelAttributePropCompletion,
116
+ inputPropCompletion,
117
+ inertiaPageCompletion,
118
+ modelCompletion,
119
+ policyCompletion,
120
+ viewCompletion,
121
+ modelMethodCompletion,
122
+ modelAttributeCompletion,
123
+ helperCompletion,
124
+ helperInputCompletion
125
+ ] = await Promise.all([
126
+ actionsCompletion(document, params.position, typeMap),
127
+ dataTypesCompletion(document, params.position, typeMap),
128
+ modelAttributePropsCompletion(document, params.position, typeMap),
129
+ inputPropsCompletion(document, params.position, typeMap),
130
+ inertiaPagesCompletion(document, params.position, typeMap),
131
+ modelsCompletion(document, params.position, typeMap),
132
+ policiesCompletion(document, params.position, typeMap),
133
+ viewsCompletion(document, params.position, typeMap),
134
+ modelMethodsCompletion(document, params.position, typeMap),
135
+ modelAttributesCompletion(document, params.position, typeMap),
136
+ helpersCompletion(document, params.position, typeMap),
137
+ helperInputsCompletion(document, params.position, typeMap)
138
+ ])
139
+
140
+ const completions = [
141
+ ...actionCompletion,
142
+ ...dataTypeCompletion,
143
+ ...modelAttributePropCompletion,
144
+ ...inputPropCompletion,
145
+ ...inertiaPageCompletion,
146
+ ...modelCompletion,
147
+ ...policyCompletion,
148
+ ...viewCompletion,
149
+ ...modelMethodCompletion,
150
+ ...modelAttributeCompletion,
151
+ ...helperCompletion,
152
+ ...helperInputCompletion
153
+ ].filter(Boolean)
70
154
 
71
155
  if (completions) {
72
156
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.0.5",
3
+ "version": "0.2.0",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -1,55 +1,40 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
- const path = require('path')
3
- const fs = require('fs')
4
- const url = require('url')
5
- module.exports = function validateActionExist(document) {
6
- const diagnostics = []
7
-
8
- if (!document.uri.endsWith('routes.js')) {
9
- return diagnostics // Return empty diagnostics if not routes.js
10
- }
11
- const actionInfo = extractActionInfo(document)
12
2
 
13
- if (!actionInfo) {
14
- return diagnostics
15
- }
3
+ module.exports = function validateActionExist(document, typeMap) {
4
+ const diagnostics = []
16
5
 
17
- const projectRoot = path.dirname(path.dirname(document.uri))
18
- const actions = extractActionInfo(document) // Get all actions
6
+ if (!document.uri.endsWith('routes.js')) return diagnostics
7
+ const actions = extractActionInfo(document)
19
8
 
20
9
  for (const { action, range } of actions) {
21
- if (isUrlOrRedirect(action)) {
22
- continue
23
- }
24
-
25
- const fullActionPath = resolveActionPath(projectRoot, action)
26
- if (!fs.existsSync(url.fileURLToPath(fullActionPath))) {
27
- const diagnostic = {
28
- severity: lsp.DiagnosticSeverity.Error,
29
- range,
30
- message: `Action '${action}' does not exist. Please check the controller file.`,
31
- source: 'Sails Validator'
32
- }
33
- diagnostics.push(diagnostic)
10
+ if (isUrlOrRedirect(action)) continue
11
+ const routeExists = Object.values(typeMap.routes || {}).some(
12
+ (route) => route.action?.name === action
13
+ )
14
+
15
+ if (!routeExists) {
16
+ diagnostics.push(
17
+ lsp.Diagnostic.create(
18
+ range,
19
+ `'${action}' action does not exist. Please check the name or create it.`,
20
+ lsp.DiagnosticSeverity.Error,
21
+ 'sails-lsp'
22
+ )
23
+ )
34
24
  }
35
25
  }
36
-
37
26
  return diagnostics
38
27
  }
39
28
 
40
29
  function extractActionInfo(document) {
41
30
  const text = document.getText()
42
-
43
- // This regex matches both object and string notations
44
- const regex = /(['"])(.+?)\1:\s*(?:{?\s*action\s*:\s*)?(['"])(.+?)\3/g
45
- let match
31
+ const regex = /(['"])(.+?)\1\s*:\s*(?:{?\s*action\s*:\s*)?(['"])(.+?)\3/g
46
32
  const actions = []
33
+ let match
47
34
 
48
35
  while ((match = regex.exec(text)) !== null) {
49
- const [fullMatch, , route, , action] = match
50
-
51
- // Store the action and its range
52
- const actionStart = match.index + fullMatch.indexOf(action)
36
+ const action = match[4]
37
+ const actionStart = match.index + match[0].lastIndexOf(action)
53
38
  const actionEnd = actionStart + action.length
54
39
 
55
40
  actions.push({
@@ -61,21 +46,13 @@ function extractActionInfo(document) {
61
46
  })
62
47
  }
63
48
 
64
- return actions // Return an array of actions
65
- }
66
-
67
- function resolveActionPath(projectRoot, actionPath) {
68
- return path.join(projectRoot, 'api', 'controllers', `${actionPath}.js`)
49
+ return actions
69
50
  }
70
51
 
71
52
  function isUrlOrRedirect(action) {
72
- if (action.startsWith('http://') || action.startsWith('https://')) {
73
- return true
74
- }
75
-
76
- if (action.startsWith('/')) {
77
- return true
78
- }
79
-
80
- return false
53
+ return (
54
+ action.startsWith('http://') ||
55
+ action.startsWith('https://') ||
56
+ action.startsWith('/')
57
+ )
81
58
  }
@@ -0,0 +1,34 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function validateDataType(document, typeMap) {
4
+ const diagnostics = []
5
+
6
+ const text = document.getText()
7
+
8
+ // Regex to match lines like: type: 'string' or type: "number"
9
+ const regex = /type\s*:\s*['"]([a-zA-Z0-9_-]+)['"]/g
10
+
11
+ let match
12
+ while ((match = regex.exec(text)) !== null) {
13
+ const dataType = match[1]
14
+ const typeStart = match.index + match[0].indexOf(dataType)
15
+ const typeEnd = typeStart + dataType.length
16
+
17
+ const isValid = typeMap.dataTypes.some((dt) => dt.type === dataType)
18
+
19
+ if (!isValid) {
20
+ diagnostics.push(
21
+ lsp.Diagnostic.create(
22
+ lsp.Range.create(
23
+ document.positionAt(typeStart),
24
+ document.positionAt(typeEnd)
25
+ ),
26
+ `'${dataType}' is not a recognized data type. Valid data types are: ${typeMap.dataTypes.map((dataType) => dataType.type).join(', ')}.`,
27
+ lsp.DiagnosticSeverity.Error,
28
+ 'sails-lsp'
29
+ )
30
+ )
31
+ }
32
+ }
33
+ return diagnostics
34
+ }
@@ -1,13 +1,51 @@
1
1
  const validateAutomigrationStrategy = require('./validate-auto-migration-strategy')
2
2
  const validateActionExist = require('./validate-action-exist')
3
+ const validatePageExist = require('./validate-page-exist')
4
+ const validateDataTypes = require('./validate-data-type')
5
+ const validatePolicyExist = require('./validate-policy-exist')
6
+ const validateModelAttributeExist = require('./validate-model-attribute-exist')
7
+ const validateViewExist = require('./validate-view-exist')
8
+ const validateHelperInputExist = require('./validate-helper-input-exist')
9
+ const validateRequiredHelperInput = require('./validate-required-helper-input')
10
+ const validateRequiredModelAttribute = require('./validate-required-model-attribute')
11
+ const validateModelExist = require('./validate-model-exist')
3
12
 
4
- module.exports = function validateDocument(connection, document) {
13
+ module.exports = function validateDocument(connection, document, typeMap) {
5
14
  const diagnostics = []
6
15
 
7
16
  const modelDiagnostics = validateAutomigrationStrategy(document)
8
- const actionDiagnostics = validateActionExist(document)
9
-
10
- diagnostics.push(...modelDiagnostics, ...actionDiagnostics)
17
+ const actionDiagnostics = validateActionExist(document, typeMap)
18
+ const pageDiagnostics = validatePageExist(document, typeMap)
19
+ const dataTypeDiagnostics = validateDataTypes(document, typeMap)
20
+ const policyDiagnostics = validatePolicyExist(document, typeMap)
21
+ const modelAttributeDiagnostics = validateModelAttributeExist(
22
+ document,
23
+ typeMap
24
+ )
25
+ const viewDiagnostics = validateViewExist(document, typeMap)
26
+ const helperInputDiagnostics = validateHelperInputExist(document, typeMap)
27
+ const requiredHelperInputDiagnostics = validateRequiredHelperInput(
28
+ document,
29
+ typeMap
30
+ )
31
+ const requiredModelAttributeDiagnostics = validateRequiredModelAttribute(
32
+ document,
33
+ typeMap
34
+ )
35
+ const modelExistDiagnostics = validateModelExist(document, typeMap)
36
+ diagnostics.push(
37
+ ...modelDiagnostics,
38
+ ...actionDiagnostics,
39
+ ...pageDiagnostics,
40
+ ...dataTypeDiagnostics,
41
+ ...policyDiagnostics,
42
+ ...modelAttributeDiagnostics,
43
+ ...viewDiagnostics,
44
+ ...helperInputDiagnostics,
45
+ ...requiredHelperInputDiagnostics,
46
+ ...requiredModelAttributeDiagnostics,
47
+ ...modelExistDiagnostics
48
+ )
11
49
 
12
50
  connection.sendDiagnostics({ uri: document.uri, diagnostics })
13
51
  }
@@ -0,0 +1,42 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function validateHelperInputExist(document, typeMap) {
4
+ const diagnostics = []
5
+ const text = document.getText()
6
+
7
+ // Regex to match sails.helpers.foo.bar.with({ ... })
8
+ // Captures: 1) helper path, 2) object literal content
9
+ const regex = /sails\.helpers((?:\.[a-zA-Z0-9_]+)+)\.with\s*\(\s*\{([^}]*)\}/g
10
+ let match
11
+ while ((match = regex.exec(text)) !== null) {
12
+ // Build helper name: e.g. .foo.bar => foo/bar
13
+ const segments = match[1].split('.').filter(Boolean)
14
+ const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
15
+ const fullHelperName = segments.map(toKebab).join('/')
16
+ const helperInfo = typeMap.helpers && typeMap.helpers[fullHelperName]
17
+ if (!helperInfo || !helperInfo.inputs) continue
18
+
19
+ // Find all property names in the object literal
20
+ const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
21
+ let propMatch
22
+ while ((propMatch = propsRegex.exec(match[2])) !== null) {
23
+ const key = propMatch[1]
24
+ if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
25
+ const start = match.index + match[0].indexOf(key)
26
+ const end = start + key.length
27
+ diagnostics.push(
28
+ lsp.Diagnostic.create(
29
+ lsp.Range.create(
30
+ document.positionAt(start),
31
+ document.positionAt(end)
32
+ ),
33
+ `Unknown input property '${key}' for helper '${fullHelperName}'.`,
34
+ lsp.DiagnosticSeverity.Error,
35
+ 'sails-lsp'
36
+ )
37
+ )
38
+ }
39
+ }
40
+ }
41
+ return diagnostics
42
+ }