@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.
- package/SailsParser.js +403 -0
- package/completions/actions-completion.js +36 -0
- package/completions/data-types-completion.js +38 -0
- package/completions/inertia-pages-completion.js +33 -0
- package/completions/input-props-completion.js +56 -0
- package/completions/model-attribute-props-completion.js +53 -0
- package/completions/model-attributes-completion.js +102 -0
- package/completions/model-methods-completion.js +61 -0
- package/completions/models-completion.js +52 -0
- package/completions/policies-completion.js +32 -0
- package/completions/views-completion.js +35 -0
- package/go-to-definitions/go-to-action.js +26 -49
- package/go-to-definitions/go-to-helper.js +33 -46
- package/go-to-definitions/go-to-model.js +39 -0
- package/go-to-definitions/go-to-page.js +38 -0
- package/go-to-definitions/go-to-policy.js +23 -72
- package/go-to-definitions/go-to-view.js +28 -55
- package/index.js +95 -20
- package/package.json +1 -1
- package/validators/validate-action-exist.js +28 -51
- package/validators/validate-data-type.js +34 -0
- package/validators/validate-document.js +21 -5
- package/validators/validate-model-attribute-exist.js +128 -0
- package/validators/validate-page-exist.js +42 -0
- package/validators/validate-policy-exist.js +45 -0
- package/completions/sails-completions.js +0 -63
- package/go-to-definitions/go-to-inertia-page.js +0 -53
- package/helpers/find-fn-line.js +0 -21
- package/helpers/find-project-root.js +0 -18
- package/helpers/find-sails.js +0 -12
- 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
|
-
|
|
37
|
-
|
|
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
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
action
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
8
|
-
return str.replace(/[A-Z]/g,
|
|
3
|
+
function toKebab(str) {
|
|
4
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
9
5
|
}
|
|
10
6
|
|
|
11
|
-
function
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|