@sailshq/language-server 0.0.5 → 0.1.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 (31) hide show
  1. package/SailsParser.js +403 -0
  2. package/completions/actions-completion.js +36 -0
  3. package/completions/data-types-completion.js +38 -0
  4. package/completions/inertia-pages-completion.js +33 -0
  5. package/completions/input-props-completion.js +56 -0
  6. package/completions/model-attribute-props-completion.js +53 -0
  7. package/completions/model-attributes-completion.js +102 -0
  8. package/completions/model-methods-completion.js +61 -0
  9. package/completions/models-completion.js +52 -0
  10. package/completions/policies-completion.js +32 -0
  11. package/completions/views-completion.js +35 -0
  12. package/go-to-definitions/go-to-action.js +26 -49
  13. package/go-to-definitions/go-to-helper.js +33 -46
  14. package/go-to-definitions/go-to-model.js +39 -0
  15. package/go-to-definitions/go-to-page.js +38 -0
  16. package/go-to-definitions/go-to-policy.js +23 -72
  17. package/go-to-definitions/go-to-view.js +28 -55
  18. package/index.js +95 -20
  19. package/package.json +1 -1
  20. package/validators/validate-action-exist.js +28 -51
  21. package/validators/validate-data-type.js +34 -0
  22. package/validators/validate-document.js +21 -5
  23. package/validators/validate-model-attribute-exist.js +128 -0
  24. package/validators/validate-page-exist.js +42 -0
  25. package/validators/validate-policy-exist.js +45 -0
  26. package/completions/sails-completions.js +0 -63
  27. package/go-to-definitions/go-to-inertia-page.js +0 -53
  28. package/helpers/find-fn-line.js +0 -21
  29. package/helpers/find-project-root.js +0 -18
  30. package/helpers/find-sails.js +0 -12
  31. package/helpers/load-sails.js +0 -39
@@ -0,0 +1,102 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function modelAttributesCompletion(
4
+ document,
5
+ position,
6
+ typeMap
7
+ ) {
8
+ const JS_FILE_TYPES = ['helpers', 'controllers', 'scripts', 'models']
9
+ const isRelevantFile = JS_FILE_TYPES.some((type) =>
10
+ document.uri.includes(`${type}/`)
11
+ )
12
+ if (!isRelevantFile) return []
13
+
14
+ const text = document.getText()
15
+ const offset = document.offsetAt(position)
16
+ const before = text.substring(0, offset)
17
+
18
+ const criteriaMatch = before.match(
19
+ /(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\s*\.\w+\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
20
+ )
21
+ const selectStringMatch = before.match(
22
+ /(?:select|omit|sort)\s*:\s*['"]([a-zA-Z0-9_]*)?$/
23
+ )
24
+ const selectArrayMatch = before.match(
25
+ /(?:select|omit|sort)\s*:\s*\[\s*['"]([a-zA-Z0-9_]*)?$/
26
+ )
27
+ const populateStringMatch = before.match(
28
+ /populate\s*:\s*['"]([a-zA-Z0-9_]*)?$/
29
+ )
30
+ const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
31
+ const sortArrayStringMatch = before.match(
32
+ /sort\s*:\s*\[\s*[^{\]]*['"]([a-zA-Z0-9_]*)?$/
33
+ )
34
+ const sortArrayObjectMatch = before.match(
35
+ /sort\s*:\s*\[\s*\{\s*([a-zA-Z0-9_]*)?$/
36
+ )
37
+
38
+ let modelName,
39
+ prefix,
40
+ attributes,
41
+ isPopulate = false
42
+
43
+ const models = typeMap.models || {}
44
+ const modelKeys = Object.keys(models)
45
+
46
+ // Better model name inference using last static model call
47
+ function inferModelName(before) {
48
+ const allMatches = [
49
+ ...before.matchAll(
50
+ /(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Z][A-Za-z0-9_]*))\s*\./g
51
+ )
52
+ ]
53
+ if (allMatches.length === 0) return null
54
+ const last = allMatches[allMatches.length - 1]
55
+ return last[1] || last[2] || null
56
+ }
57
+
58
+ if (criteriaMatch) {
59
+ modelName = criteriaMatch[1] || criteriaMatch[2]
60
+ prefix = criteriaMatch[3] || ''
61
+ } else if (selectStringMatch || selectArrayMatch) {
62
+ modelName = inferModelName(before)
63
+ prefix = (selectStringMatch || selectArrayMatch)[1] || ''
64
+ } else if (populateStringMatch) {
65
+ isPopulate = true
66
+ modelName = inferModelName(before)
67
+ prefix = populateStringMatch[1] || ''
68
+ } else if (sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch) {
69
+ modelName = inferModelName(before)
70
+ prefix =
71
+ (sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch)[1] || ''
72
+ } else {
73
+ return []
74
+ }
75
+
76
+ if (!modelName) return []
77
+ const foundKey = modelKeys.find(
78
+ (k) => k.toLowerCase() === modelName.toLowerCase()
79
+ )
80
+ const model = foundKey ? models[foundKey] : null
81
+ if (!model) return []
82
+
83
+ if (isPopulate) {
84
+ attributes = Object.entries(model.attributes || {})
85
+ .filter(([, def]) => def && (def.model || def.collection))
86
+ .map(([attr]) => attr)
87
+ } else {
88
+ attributes = Object.keys(model.attributes || {})
89
+ }
90
+
91
+ return attributes
92
+ .filter((attr) => attr.startsWith(prefix))
93
+ .map((attr) => ({
94
+ label: attr,
95
+ kind: lsp.CompletionItemKind.Field,
96
+ detail: `Attribute of ${modelName}`,
97
+ documentation: `${modelName}.${attr}`,
98
+ sortText: attr,
99
+ filterText: attr,
100
+ insertText: attr
101
+ }))
102
+ }
@@ -0,0 +1,61 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function modelMethodsCompletion(document, position, typeMap) {
4
+ const JS_FILE_TYPES = ['helpers', 'controllers', 'scripts', 'models']
5
+ const isRelevantFile = JS_FILE_TYPES.some((type) =>
6
+ document.uri.includes(`${type}/`)
7
+ )
8
+ if (!isRelevantFile) return []
9
+
10
+ const text = document.getText()
11
+ const offset = document.offsetAt(position)
12
+ const before = text.substring(0, offset)
13
+
14
+ // Match static calls like User.method or sails.models.user.method
15
+ const staticCallMatch = before.match(
16
+ /(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\.\s*([a-zA-Z]*)?$/
17
+ )
18
+ // Match chainable calls like User.find().<chainable> or User.find({...}).<chainable>
19
+ const chainableCallMatch = before.match(
20
+ /([A-Za-z_$][\w$]*)\.[a-zA-Z_]+\([^)]*\)\.\s*([a-zA-Z]*)?$/
21
+ )
22
+
23
+ let modelName, prefix, methods
24
+
25
+ // Make model lookup case-insensitive
26
+ const models = typeMap.models || {}
27
+ const modelKeys = Object.keys(models)
28
+
29
+ if (chainableCallMatch) {
30
+ modelName = chainableCallMatch[1]
31
+ prefix = chainableCallMatch[2] || ''
32
+ if (!modelName) return []
33
+ const foundKey = modelKeys.find(
34
+ (k) => k.toLowerCase() === modelName.toLowerCase()
35
+ )
36
+ methods = foundKey ? models[foundKey].chainableMethods || [] : []
37
+ } else if (staticCallMatch) {
38
+ modelName = staticCallMatch[1] || staticCallMatch[2]
39
+ prefix = staticCallMatch[3] || ''
40
+ if (!modelName) return []
41
+ const foundKey = modelKeys.find(
42
+ (k) => k.toLowerCase() === modelName.toLowerCase()
43
+ )
44
+ methods = foundKey ? models[foundKey].methods || [] : []
45
+ } else {
46
+ return []
47
+ }
48
+
49
+ return methods
50
+ .filter((method) => method.name.startsWith(prefix))
51
+ .map((method) => ({
52
+ label: method.name,
53
+ kind: lsp.CompletionItemKind.Method,
54
+ detail: method.description,
55
+ documentation: `${modelName}.${method.name}()`,
56
+ sortText: method.name,
57
+ filterText: method.name,
58
+ insertText: method.name + '($0)',
59
+ insertTextFormat: lsp.InsertTextFormat.Snippet
60
+ }))
61
+ }
@@ -0,0 +1,52 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function modelsCompletion(document, position, typeMap) {
4
+ const text = document.getText()
5
+ const offset = document.offsetAt(position)
6
+ const before = text.substring(0, offset)
7
+
8
+ // sails.models.<model>
9
+ const dotMatch = before.match(/sails\.models\.([a-zA-Z0-9_]*)$/)
10
+ if (dotMatch) {
11
+ const prefix = dotMatch[1] || ''
12
+
13
+ return Object.entries(typeMap.models || {})
14
+ .map(([modelName, model]) => {
15
+ const key = modelName.toLowerCase()
16
+ if (!key.startsWith(prefix.toLowerCase())) return null
17
+
18
+ return {
19
+ label: key,
20
+ kind: lsp.CompletionItemKind.Class,
21
+ detail: 'Sails Model (sails.models)',
22
+ documentation: model.src?.replace(/^.*\/api\/models\//, ''),
23
+ insertText: key
24
+ }
25
+ })
26
+ .filter(Boolean)
27
+ }
28
+
29
+ // await User.<method>
30
+ // Only match if there's an uppercase prefix and it follows "await"
31
+ const awaitMatch = before.match(/(?:await\s+)([A-Z][a-zA-Z0-9_]*)$/)
32
+ if (awaitMatch) {
33
+ const prefix = awaitMatch[1]
34
+ if (!prefix) return [] // Don't show anything if prefix is empty
35
+
36
+ return Object.entries(typeMap.models || {})
37
+ .map(([modelName, model]) => {
38
+ if (!modelName.startsWith(prefix)) return null
39
+
40
+ return {
41
+ label: modelName,
42
+ kind: lsp.CompletionItemKind.Class,
43
+ detail: 'Sails Model',
44
+ documentation: model.path?.replace(/^.*\/api\/models\//, ''),
45
+ insertText: modelName
46
+ }
47
+ })
48
+ .filter(Boolean)
49
+ }
50
+
51
+ return []
52
+ }
@@ -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,60 +1,47 @@
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
11
+ // Match sails.helpers.foo or sails.helpers.bar.baz
12
+ const regex =
13
+ /\bsails\.helpers(?:\.(?<group>[a-zA-Z0-9_]+))?\.(?<helper>[a-zA-Z0-9_]+)(?![\w.])/g
14
+
47
15
  let match
48
16
 
49
17
  while ((match = regex.exec(text)) !== null) {
50
- const start = match.index
51
- const end = start + match[0].length
52
-
53
- if (start <= offset && offset <= end) {
54
- const helperPath = match[1].split('.').filter((part) => part !== 'with')
55
- return { helperPath }
18
+ const { group, helper } = match.groups
19
+
20
+ const kebabGroup = group ? toKebab(group) : null
21
+ const kebabHelper = toKebab(helper)
22
+ const fullHelperName = kebabGroup
23
+ ? `${kebabGroup}/${kebabHelper}`
24
+ : kebabHelper
25
+
26
+ // Compute accurate range for just the helper name
27
+ const helperStart = match.index + match[0].lastIndexOf(helper)
28
+ const helperEnd = helperStart + helper.length
29
+
30
+ if (offset >= helperStart && offset <= helperEnd) {
31
+ const helperInfo = typeMap.helpers?.[fullHelperName]
32
+ if (helperInfo && helperInfo.path) {
33
+ const uri = `file://${helperInfo.path}`
34
+ return lsp.LocationLink.create(
35
+ uri,
36
+ lsp.Range.create(helperInfo.fnLine - 1, 0, helperInfo.fnLine - 1, 0),
37
+ lsp.Range.create(helperInfo.fnLine - 1, 0, helperInfo.fnLine - 1, 0),
38
+ lsp.Range.create(
39
+ document.positionAt(helperStart),
40
+ document.positionAt(helperEnd)
41
+ )
42
+ )
43
+ }
56
44
  }
57
45
  }
58
-
59
46
  return null
60
47
  }
@@ -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
- }