@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.
- package/SailsParser.js +652 -0
- package/completions/actions-completion.js +36 -0
- package/completions/data-types-completion.js +39 -0
- package/completions/helper-inputs-completion.js +91 -0
- package/completions/helpers-completion.js +85 -0
- package/completions/inertia-pages-completion.js +33 -0
- package/completions/input-props-completion.js +52 -0
- package/completions/model-attribute-props-completion.js +57 -0
- package/completions/model-attributes-completion.js +195 -0
- package/completions/model-methods-completion.js +71 -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 +37 -45
- 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 +103 -19
- 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 +42 -4
- package/validators/validate-helper-input-exist.js +42 -0
- package/validators/validate-model-attribute-exist.js +297 -0
- package/validators/validate-model-exist.js +64 -0
- package/validators/validate-page-exist.js +42 -0
- package/validators/validate-policy-exist.js +45 -0
- package/validators/validate-required-helper-input.js +49 -0
- package/validators/validate-required-model-attribute.js +56 -0
- package/validators/validate-view-exist.js +86 -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,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,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
|
|
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
|
-
const regex =
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
35
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
}
|