@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
@@ -0,0 +1,32 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function policiesCompletion(document, position, typeMap) {
4
+ if (!document.uri.endsWith('policies.js')) return []
5
+
6
+ const text = document.getText()
7
+ const offset = document.offsetAt(position)
8
+ const before = text.substring(0, offset)
9
+
10
+ // Match inside string value or array of strings:
11
+ // 'isLog|' or ['isLog|'] or '*': 'isLog|'
12
+ const match = before.match(/:\s*(?:\[)?\s*['"]([^'"]*)$/)
13
+ if (!match) return []
14
+
15
+ const prefix = match[1]
16
+
17
+ return Object.entries(typeMap.policies || {})
18
+ .map(([policyName, policy]) => {
19
+ if (!policyName.startsWith(prefix)) return null
20
+
21
+ return {
22
+ label: policyName,
23
+ kind: lsp.CompletionItemKind.Function,
24
+ detail: 'Policy',
25
+ documentation: policy.path,
26
+ sortText: policyName,
27
+ filterText: policyName,
28
+ insertText: policyName
29
+ }
30
+ })
31
+ .filter(Boolean)
32
+ }
@@ -0,0 +1,35 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function viewsCompletion(document, position, typeMap) {
4
+ const uri = document.uri
5
+ if (!uri.endsWith('routes.js') && !uri.includes('/api/controllers/'))
6
+ return []
7
+
8
+ const text = document.getText()
9
+ const offset = document.offsetAt(position)
10
+ const before = text.substring(0, offset)
11
+
12
+ // Match { view: '...' } or viewTemplatePath: '...'
13
+ const match = before.match(/\b(view|viewTemplatePath)\s*:\s*['"]([^'"]*)$/)
14
+ if (!match) return []
15
+
16
+ const prefix = match[2]
17
+
18
+ const completions = Object.entries(typeMap.views || {})
19
+ .map(([viewKey, viewData]) => {
20
+ if (!viewKey.startsWith(prefix)) return null
21
+
22
+ return {
23
+ label: viewKey,
24
+ kind: lsp.CompletionItemKind.File,
25
+ detail: 'View',
26
+ documentation: viewData.path,
27
+ sortText: viewKey,
28
+ filterText: viewKey,
29
+ insertText: viewKey
30
+ }
31
+ })
32
+ .filter(Boolean)
33
+
34
+ return completions
35
+ }
@@ -1,66 +1,43 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
- const findFnLine = require('../helpers/find-fn-line')
4
3
 
5
- module.exports = async function goToAction(document, position) {
4
+ module.exports = async function goToAction(document, position, typeMap) {
6
5
  const fileName = path.basename(document.uri)
6
+ if (fileName !== 'routes.js') return null
7
7
 
8
- if (fileName !== 'routes.js') {
9
- return null
10
- }
11
- const actionInfo = extractActionInfo(document, position)
12
-
13
- if (!actionInfo) {
14
- return null
15
- }
16
-
17
- const projectRoot = path.dirname(path.dirname(document.uri))
18
-
19
- const fullActionPath = resolveActionPath(projectRoot, actionInfo.action)
20
-
21
- if (fullActionPath) {
22
- const fnLineNumber = await findFnLine(fullActionPath)
23
- return lsp.Location.create(
24
- fullActionPath,
25
- lsp.Range.create(fnLineNumber, 0, fnLineNumber, 0)
26
- )
27
- }
28
-
29
- return null
30
- }
31
-
32
- function extractActionInfo(document, position) {
33
8
  const text = document.getText()
34
9
  const offset = document.offsetAt(position)
35
10
 
36
- // This regex matches both object and string notations
37
- const regex = /(['"])(.+?)\1:\s*(?:{?\s*action\s*:\s*)?(['"])(.+?)\3/g
11
+ const regex =
12
+ /:\s*(?:{[^}]*?\baction\s*:\s*(?<quote>['"])(?<action>[^'"]+)\k<quote>[^}]*?}|(?<quoteAlt>['"])(?<actionAlt>[^'"]+)\k<quoteAlt>)/g
13
+
38
14
  let match
39
15
 
40
16
  while ((match = regex.exec(text)) !== null) {
41
- const [fullMatch, , route, , action] = match
42
- const start = match.index
43
- const end = start + fullMatch.length
44
-
45
- // Check if the cursor is anywhere within the entire match
46
- if (start <= offset && offset <= end) {
47
- // Find the start and end positions of the action part
48
- const actionStart = text.indexOf(action, start)
49
- const actionEnd = actionStart + action.length
50
-
51
- return {
52
- action,
53
- range: lsp.Range.create(
54
- document.positionAt(actionStart),
55
- document.positionAt(actionEnd)
17
+ const actionName = match.groups.action || match.groups.actionAlt
18
+ const quote = match.groups.quote || match.groups.quoteAlt
19
+ const fullMatchStart =
20
+ match.index + match[0].indexOf(quote + actionName + quote)
21
+ const fullMatchEnd = fullMatchStart + actionName.length + 2 // +2 for quotes
22
+
23
+ if (offset >= fullMatchStart && offset <= fullMatchEnd) {
24
+ const routeEntry = Object.values(typeMap.routes).find(
25
+ (route) => route.action?.name === actionName
26
+ )
27
+ if (routeEntry?.action) {
28
+ const { path: actionPath, fnLine } = routeEntry.action
29
+ const uri = `file://${actionPath}`
30
+ return lsp.LocationLink.create(
31
+ uri,
32
+ lsp.Range.create(fnLine - 1, 0, fnLine - 1, 0),
33
+ lsp.Range.create(fnLine - 1, 0, fnLine - 1, 0),
34
+ lsp.Range.create(
35
+ document.positionAt(fullMatchStart),
36
+ document.positionAt(fullMatchEnd)
37
+ )
56
38
  )
57
39
  }
58
40
  }
59
41
  }
60
-
61
42
  return null
62
43
  }
63
-
64
- function resolveActionPath(projectRoot, actionPath) {
65
- return path.join(projectRoot, 'api', 'controllers', `${actionPath}.js`)
66
- }
@@ -1,58 +1,50 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
- const path = require('path')
3
- const fs = require('fs').promises
4
- const findProjectRoot = require('../helpers/find-project-root')
5
- const findFnLine = require('../helpers/find-fn-line')
6
2
 
7
- function camelToKebabCase(str) {
8
- return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
3
+ function toKebab(str) {
4
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
9
5
  }
10
6
 
11
- function normalizeHelperPath(helperPath) {
12
- const parts = helperPath.split('/')
13
- const fileName = parts.pop() // Get the last part (file name)
14
- const normalizedFileName = camelToKebabCase(fileName)
15
- return [...parts, normalizedFileName].join('/')
16
- }
17
-
18
- module.exports = async function goToHelper(document, position) {
19
- const helperInfo = extractHelperInfo(document, position)
20
-
21
- if (!helperInfo) {
22
- return null
23
- }
24
-
25
- const projectRoot = await findProjectRoot(document.uri)
26
- const normalizedHelperPath = normalizeHelperPath(
27
- helperInfo.helperPath.join('/')
28
- )
29
- const fullHelperPath =
30
- path.join(projectRoot, 'api', 'helpers', normalizedHelperPath) + '.js'
31
- if (fullHelperPath) {
32
- const fnLineNumber = await findFnLine(fullHelperPath)
33
- return lsp.Location.create(
34
- fullHelperPath,
35
- lsp.Range.create(fnLineNumber, 0, fnLineNumber, 0)
36
- )
37
- }
38
- }
39
-
40
- function extractHelperInfo(document, position) {
7
+ module.exports = async function goToHelper(document, position, typeMap) {
41
8
  const text = document.getText()
42
9
  const offset = document.offsetAt(position)
43
10
 
44
- // Regular expression to match sails.helpers.exampleHelper() or sails.helpers.exampleHelper.with()
45
- // Also matches nested helpers like sails.helpers.mail.send() or sails.helpers.mail.send.with()
46
- const regex = /sails\.helpers\.([a-zA-Z0-9.]+)(?:\.with)?\s*\(/g
47
- let match
11
+ // Regex to match sails.helpers.foo.bar (even if chained, e.g. .with, .with(), .with({}), etc.)
12
+ // This is similar to go-to-model: match the helper path, then allow any chain after
13
+ const regex = /\bsails\.helpers((?:\.[A-Za-z0-9_]+)+)/g
48
14
 
15
+ let match
49
16
  while ((match = regex.exec(text)) !== null) {
50
- const start = match.index
51
- const end = start + match[0].length
17
+ const segments = match[1].slice(1).split('.') // drop the leading dot
18
+ if (!segments.length) continue
19
+
20
+ // Only use the last segment before .with as the helper name
21
+ let cleanSegments = segments
22
+ if (segments[segments.length - 1] === 'with') {
23
+ cleanSegments = segments.slice(0, -1)
24
+ }
25
+ const fullHelperName = cleanSegments.map(toKebab).join('/')
26
+ const lastSeg = cleanSegments[cleanSegments.length - 1]
27
+ const helperStart = match.index + match[0].lastIndexOf(lastSeg)
28
+ const helperEnd = helperStart + lastSeg.length
29
+
30
+ // Allow go-to if the cursor is anywhere inside the helper name
31
+ if (offset < helperStart || offset > helperEnd) {
32
+ continue
33
+ }
52
34
 
53
- if (start <= offset && offset <= end) {
54
- const helperPath = match[1].split('.').filter((part) => part !== 'with')
55
- return { helperPath }
35
+ // Now look up in your typeMap
36
+ const info = typeMap.helpers?.[fullHelperName]
37
+ if (info?.path) {
38
+ const uri = `file://${info.path}`
39
+ return lsp.LocationLink.create(
40
+ uri,
41
+ lsp.Range.create(info.fnLine - 1, 0, info.fnLine - 1, 0),
42
+ lsp.Range.create(info.fnLine - 1, 0, info.fnLine - 1, 0),
43
+ lsp.Range.create(
44
+ document.positionAt(helperStart),
45
+ document.positionAt(helperEnd)
46
+ )
47
+ )
56
48
  }
57
49
  }
58
50
 
@@ -0,0 +1,39 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = async function goToModel(document, position, typeMap) {
4
+ const text = document.getText()
5
+ const offset = document.offsetAt(position)
6
+
7
+ const regex =
8
+ /\b(?:(?<classModel>[A-Z][a-zA-Z0-9_]*)|sails\.models\.(?<dotModel>[a-z][a-zA-Z0-9_]*))\s*\.\s*\w*/g
9
+
10
+ let match
11
+ while ((match = regex.exec(text)) !== null) {
12
+ const modelNameRaw = match.groups.classModel || match.groups.dotModel
13
+ const modelName =
14
+ match.groups.classModel ||
15
+ (match.groups.dotModel &&
16
+ match.groups.dotModel.charAt(0).toUpperCase() +
17
+ match.groups.dotModel.slice(1))
18
+
19
+ if (!modelName) continue
20
+
21
+ const start = match.index + match[0].indexOf(modelNameRaw)
22
+ const end = start + modelNameRaw.length
23
+
24
+ if (offset >= start && offset <= end) {
25
+ const model = typeMap.models?.[modelName]
26
+ if (!model?.path) return null
27
+
28
+ const uri = `file://${model.path}`
29
+ return lsp.LocationLink.create(
30
+ uri,
31
+ lsp.Range.create(0, 0, 0, 0), // target range (usually top of file)
32
+ lsp.Range.create(0, 0, 0, 0), // target selection range
33
+ lsp.Range.create(document.positionAt(start), document.positionAt(end)) // origin range
34
+ )
35
+ }
36
+ }
37
+
38
+ return null
39
+ }
@@ -0,0 +1,38 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ module.exports = async function goToPage(document, position, typeMap) {
3
+ const filePath = document.uri
4
+ if (!filePath.includes('/api/controllers/')) return null
5
+ const text = document.getText()
6
+ const offset = document.offsetAt(position)
7
+
8
+ const regex =
9
+ /{[^}]*?\bpage\s*:\s*(?<quote>['"])(?<page>[^'"]+)\k<quote>[^}]*?}/g
10
+
11
+ let match
12
+
13
+ while ((match = regex.exec(text)) !== null) {
14
+ const pageName = match.groups.page
15
+ const quote = match.groups.quote
16
+ const fullMatchStart =
17
+ match.index + match[0].indexOf(quote + pageName + quote)
18
+ const fullMatchEnd = fullMatchStart + pageName.length + 2 // +2 for quotes
19
+
20
+ if (offset >= fullMatchStart && offset <= fullMatchEnd) {
21
+ const pagePath = typeMap.pages?.[pageName]
22
+ if (pagePath) {
23
+ const uri = `file://${pagePath.path}`
24
+ return lsp.LocationLink.create(
25
+ uri,
26
+ lsp.Range.create(0, 0, 0, 0),
27
+ lsp.Range.create(0, 0, 0, 0),
28
+ lsp.Range.create(
29
+ document.positionAt(fullMatchStart),
30
+ document.positionAt(fullMatchEnd)
31
+ )
32
+ )
33
+ }
34
+ }
35
+ }
36
+
37
+ return null
38
+ }
@@ -1,90 +1,41 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
- const fs = require('fs').promises
4
3
 
5
- module.exports = async function goToPolicy(document, position) {
4
+ module.exports = async function goToPolicy(document, position, typeMap) {
6
5
  const fileName = path.basename(document.uri)
6
+ if (fileName !== 'policies.js') return null
7
7
 
8
- if (fileName !== 'policies.js') {
9
- return null
10
- }
11
-
12
- const policyInfo = extractPolicyInfo(document, position)
13
-
14
- if (!policyInfo) {
15
- return null
16
- }
17
-
18
- const projectRoot = path.dirname(path.dirname(document.uri))
19
- const fullPolicyPath = resolvePolicyPath(projectRoot, policyInfo.policy)
20
-
21
- if (await fileExists(fullPolicyPath)) {
22
- return lsp.Location.create(fullPolicyPath, lsp.Range.create(0, 0, 0, 0))
23
- }
24
-
25
- return null
26
- }
27
-
28
- function extractPolicyInfo(document, position) {
29
8
  const text = document.getText()
30
9
  const offset = document.offsetAt(position)
31
10
 
32
- // This regex matches policy definitions, including arrays of policies and boolean values
33
11
  const regex =
34
- /(['"])((?:\*|[\w-]+(?:\/\*?)?))?\1\s*:\s*((?:\[?\s*(?:(?:['"][\w-]+['"](?:\s*,\s*)?)+)\s*\]?)|true|false)/g
12
+ /:\s*(\[\s*)?(?<quote>['"])(?<policy>[^'"]+)\k<quote>(\s*,\s*['"][^'"]+['"])*(\s*\])?/g
13
+
35
14
  let match
36
15
 
37
16
  while ((match = regex.exec(text)) !== null) {
38
- const [fullMatch, , route, policiesOrBoolean] = match
39
- const start = match.index
40
- const end = start + fullMatch.length
41
-
42
- // Check if the cursor is anywhere within the entire match
43
- if (start <= offset && offset <= end) {
44
- // If policiesOrBoolean is a boolean, ignore it
45
- if (policiesOrBoolean === true || policiesOrBoolean === false) {
46
- continue
47
- }
48
-
49
- // Remove brackets if present and split into individual policies
50
- const policies = policiesOrBoolean
51
- .replace(/^\[|\]$/g, '')
52
- .split(',')
53
- .map((p) => p.trim().replace(/^['"]|['"]$/g, ''))
54
-
55
- // Find which policy the cursor is on
56
- let currentStart = start + fullMatch.indexOf(policiesOrBoolean)
57
- for (const policy of policies) {
58
- const policyStart = text.indexOf(policy, currentStart)
59
- const policyEnd = policyStart + policy.length
60
-
61
- if (offset >= policyStart && offset <= policyEnd) {
62
- return {
63
- policy,
64
- range: lsp.Range.create(
65
- document.positionAt(policyStart),
66
- document.positionAt(policyEnd)
67
- )
68
- }
69
- }
70
-
71
- currentStart = policyEnd
17
+ const policyName = match.groups.policy
18
+ const quote = match.groups.quote
19
+ const fullMatchStart =
20
+ match.index + match[0].indexOf(quote + policyName + quote)
21
+ const fullMatchEnd = fullMatchStart + policyName.length + 2
22
+
23
+ if (offset >= fullMatchStart && offset <= fullMatchEnd) {
24
+ const policyPath = typeMap.policies?.[policyName]
25
+ if (policyPath) {
26
+ const uri = `file://${policyPath.path}`
27
+ return lsp.LocationLink.create(
28
+ uri,
29
+ lsp.Range.create(0, 0, 0, 0),
30
+ lsp.Range.create(0, 0, 0, 0),
31
+ lsp.Range.create(
32
+ document.positionAt(fullMatchStart),
33
+ document.positionAt(fullMatchEnd)
34
+ )
35
+ )
72
36
  }
73
37
  }
74
38
  }
75
39
 
76
40
  return null
77
41
  }
78
-
79
- function resolvePolicyPath(projectRoot, policyPath) {
80
- return path.join(projectRoot, 'api', 'policies', `${policyPath}.js`)
81
- }
82
-
83
- async function fileExists(filePath) {
84
- try {
85
- await fs.access(new URL(filePath))
86
- return true
87
- } catch {
88
- return false
89
- }
90
- }
@@ -1,70 +1,43 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
- const fs = require('fs').promises
4
3
 
5
- const findProjectRoot = require('../helpers/find-project-root')
6
-
7
- module.exports = async function goToView(document, position) {
8
- const viewInfo = extractViewInfo(document, position)
9
-
10
- if (!viewInfo) {
11
- return null
12
- }
13
-
14
- const projectRoot = await findProjectRoot(document.uri)
15
-
16
- const fullViewPath = resolveViewPath(projectRoot, viewInfo.view)
17
-
18
- try {
19
- await fs.access(fullViewPath)
20
- return lsp.Location.create(fullViewPath, lsp.Range.create(0, 0, 0, 0))
21
- } catch (error) {
22
- return null
23
- }
24
- }
25
-
26
- function resolveViewPath(projectRoot, viewPath) {
27
- return path.join(projectRoot, 'views', `${viewPath}.ejs`)
28
- }
29
-
30
- function extractViewInfo(document, position) {
4
+ module.exports = async function goToView(document, position, typeMap) {
5
+ const fileName = path.basename(document.uri)
6
+ const filePath = document.uri
31
7
  const text = document.getText()
32
8
  const offset = document.offsetAt(position)
33
9
 
34
- // This regex matches both object notation for views and viewTemplatePath
35
- const regex =
36
- /(?:(['"])(.+?)\1\s*:\s*{\s*view\s*:\s*(['"])(.+?)\3\s*}|viewTemplatePath\s*:\s*(['"])(.+?)\5)/g
37
- let match
10
+ const isRoutes = fileName === 'routes.js'
11
+ const isController = filePath.includes('/api/controllers/')
38
12
 
39
- while ((match = regex.exec(text)) !== null) {
40
- const [fullMatch, , , , viewInObject, , viewInController] = match
41
- const view = viewInObject || viewInController
42
- const start = match.index
43
- const end = start + fullMatch.length
13
+ if (!isRoutes && !isController) return null
44
14
 
45
- // Check if the cursor is anywhere within the entire match
46
- if (start <= offset && offset <= end) {
47
- // Find the start and end positions of the view part, including quotes
48
- const viewStartWithQuote = text.lastIndexOf(
49
- "'",
50
- text.indexOf(view, start)
51
- ) // Find the opening quote
52
- const viewEndWithQuote =
53
- text.indexOf("'", text.indexOf(view, start) + view.length) + 1 // Find the closing quote and include it
15
+ const regex =
16
+ /\b(viewTemplatePath|view)\s*:\s*(?<quote>['"])(?<view>[^'"]+)\k<quote>/g
54
17
 
55
- return {
56
- view,
57
- range: lsp.Range.create(
58
- document.positionAt(viewStartWithQuote),
59
- document.positionAt(viewEndWithQuote)
18
+ let match
19
+ while ((match = regex.exec(text)) !== null) {
20
+ const viewName = match.groups.view
21
+ const quote = match.groups.quote
22
+ const fullMatchStart =
23
+ match.index + match[0].indexOf(quote + viewName + quote)
24
+ const fullMatchEnd = fullMatchStart + viewName.length + 2
25
+
26
+ if (offset >= fullMatchStart && offset <= fullMatchEnd) {
27
+ const viewPath = typeMap.views?.[viewName]
28
+ if (viewPath) {
29
+ const uri = `file://${viewPath.path}`
30
+ return lsp.LocationLink.create(
31
+ uri,
32
+ lsp.Range.create(0, 0, 0, 0),
33
+ lsp.Range.create(0, 0, 0, 0),
34
+ lsp.Range.create(
35
+ document.positionAt(fullMatchStart),
36
+ document.positionAt(fullMatchEnd)
37
+ )
60
38
  )
61
39
  }
62
40
  }
63
41
  }
64
-
65
42
  return null
66
43
  }
67
-
68
- function resolveViewPath(projectRoot, viewPath) {
69
- return path.join(projectRoot, 'views', `${viewPath}.ejs`)
70
- }