@sailshq/language-server 0.1.0 → 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 +325 -76
- package/completions/data-types-completion.js +6 -5
- package/completions/helper-inputs-completion.js +91 -0
- package/completions/helpers-completion.js +85 -0
- package/completions/input-props-completion.js +19 -23
- package/completions/model-attribute-props-completion.js +36 -32
- package/completions/model-attributes-completion.js +103 -10
- package/completions/model-methods-completion.js +20 -10
- package/go-to-definitions/go-to-helper.js +34 -29
- package/index.js +12 -3
- package/package.json +1 -1
- package/validators/validate-document.js +23 -1
- package/validators/validate-helper-input-exist.js +42 -0
- package/validators/validate-model-attribute-exist.js +278 -109
- package/validators/validate-model-exist.js +64 -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
|
@@ -13,12 +13,17 @@ module.exports = function inputPropsCompletion(document, position, typeMap) {
|
|
|
13
13
|
const offset = document.offsetAt(position)
|
|
14
14
|
const before = text.substring(0, offset)
|
|
15
15
|
|
|
16
|
+
const lines = before.split('\n')
|
|
17
|
+
const lastLine = lines[lines.length - 1]
|
|
18
|
+
|
|
19
|
+
// Only trigger if we're on a new line with optional whitespace (no code)
|
|
20
|
+
if (!/^\s*$/.test(lastLine)) return []
|
|
21
|
+
|
|
16
22
|
// Check we're inside the inputs: { ... } section
|
|
17
23
|
const insideInputs = /inputs\s*:\s*{[\s\S]*$/.test(before)
|
|
18
24
|
if (!insideInputs) return []
|
|
19
25
|
|
|
20
|
-
// Walk backward to see if we're inside an input
|
|
21
|
-
const lines = before.split('\n')
|
|
26
|
+
// Walk backward to see if we're inside an input block
|
|
22
27
|
const reversed = lines.slice().reverse()
|
|
23
28
|
let insideInputBlock = false
|
|
24
29
|
|
|
@@ -28,29 +33,20 @@ module.exports = function inputPropsCompletion(document, position, typeMap) {
|
|
|
28
33
|
insideInputBlock = true
|
|
29
34
|
break
|
|
30
35
|
}
|
|
31
|
-
if (/^\}/.test(trimmed))
|
|
32
|
-
break // exited a block
|
|
33
|
-
}
|
|
36
|
+
if (/^\}/.test(trimmed)) break
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
if (!insideInputBlock) return []
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
? lsp.CompletionItemKind.Method
|
|
50
|
-
: lsp.CompletionItemKind.Field,
|
|
51
|
-
detail,
|
|
52
|
-
documentation: detail,
|
|
53
|
-
insertText: `${label}: `,
|
|
54
|
-
insertTextFormat: lsp.InsertTextFormat.PlainText
|
|
55
|
-
}))
|
|
41
|
+
return typeMap.inputProps.map(({ label, detail }) => ({
|
|
42
|
+
label,
|
|
43
|
+
kind:
|
|
44
|
+
label === 'custom'
|
|
45
|
+
? lsp.CompletionItemKind.Method
|
|
46
|
+
: lsp.CompletionItemKind.Field,
|
|
47
|
+
detail,
|
|
48
|
+
documentation: detail,
|
|
49
|
+
insertText: `${label}: `,
|
|
50
|
+
insertTextFormat: lsp.InsertTextFormat.PlainText
|
|
51
|
+
}))
|
|
56
52
|
}
|
|
@@ -6,48 +6,52 @@ module.exports = function modelAttributePropsCompletion(
|
|
|
6
6
|
typeMap
|
|
7
7
|
) {
|
|
8
8
|
const filePath = document.uri
|
|
9
|
+
if (!filePath.includes('/api/models/')) return []
|
|
9
10
|
|
|
10
|
-
const isTargetFile = filePath.includes('/api/models/')
|
|
11
|
-
if (!isTargetFile) return []
|
|
12
11
|
const text = document.getText()
|
|
13
12
|
const offset = document.offsetAt(position)
|
|
14
13
|
const before = text.substring(0, offset)
|
|
14
|
+
const lines = before.split('\n')
|
|
15
|
+
const lastLine = lines[lines.length - 1]
|
|
16
|
+
|
|
17
|
+
// Only trigger on an empty or whitespace-only line
|
|
18
|
+
if (!/^\s*$/.test(lastLine)) return []
|
|
15
19
|
|
|
16
20
|
// Confirm we're inside the attributes section
|
|
17
|
-
const insideAttributes = /attributes\s*:\s*{[\s\S]
|
|
21
|
+
const insideAttributes = /attributes\s*:\s*{([\s\S]*)$/.exec(before)
|
|
18
22
|
if (!insideAttributes) return []
|
|
19
23
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
// Use a stack to track braces and find if we're inside a property block
|
|
25
|
+
let braceStack = []
|
|
26
|
+
let insideProperty = false
|
|
27
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
+
const line = lines[i]
|
|
29
|
+
for (let j = line.length - 1; j >= 0; j--) {
|
|
30
|
+
if (line[j] === '}') braceStack.push('}')
|
|
31
|
+
if (line[j] === '{') {
|
|
32
|
+
if (braceStack.length > 0) {
|
|
33
|
+
braceStack.pop()
|
|
34
|
+
} else {
|
|
35
|
+
const propMatch = lines[i]
|
|
36
|
+
.slice(0, j + 1)
|
|
37
|
+
.match(/([a-zA-Z0-9_]+)\s*:\s*{$/)
|
|
38
|
+
if (propMatch) insideProperty = true
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
33
42
|
}
|
|
43
|
+
if (insideProperty) break
|
|
44
|
+
if (/^\s*attributes\s*:\s*{/.test(line)) break
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
// Optional: match current prefix
|
|
39
|
-
const lastLine = lines[lines.length - 1]
|
|
40
|
-
const prefixMatch = lastLine.match(/([a-zA-Z0-9_]*)$/)
|
|
41
|
-
const prefix = prefixMatch ? prefixMatch[1] : ''
|
|
47
|
+
if (!insideProperty) return []
|
|
42
48
|
|
|
43
|
-
return typeMap.modelAttributeProps
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
insertTextFormat: lsp.InsertTextFormat.PlainText
|
|
52
|
-
}))
|
|
49
|
+
return typeMap.modelAttributeProps.map(({ label, detail }) => ({
|
|
50
|
+
label,
|
|
51
|
+
kind: lsp.CompletionItemKind.Field,
|
|
52
|
+
detail,
|
|
53
|
+
documentation: detail,
|
|
54
|
+
insertText: `${label}: `,
|
|
55
|
+
insertTextFormat: lsp.InsertTextFormat.PlainText
|
|
56
|
+
}))
|
|
53
57
|
}
|
|
@@ -29,12 +29,71 @@ module.exports = function modelAttributesCompletion(
|
|
|
29
29
|
)
|
|
30
30
|
const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
|
|
31
31
|
const sortArrayStringMatch = before.match(
|
|
32
|
-
/sort\s*:\s*\[\s*[
|
|
32
|
+
/sort\s*:\s*\[\s*[^\{\]]*['"]([a-zA-Z0-9_]*)?$/
|
|
33
33
|
)
|
|
34
34
|
const sortArrayObjectMatch = before.match(
|
|
35
35
|
/sort\s*:\s*\[\s*\{\s*([a-zA-Z0-9_]*)?$/
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
// Detect if we are inside a .select(['']), .omit(['']), .sort(['']), etc. chainable method call context
|
|
39
|
+
// This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
|
|
40
|
+
const chainableMethodCallRegex =
|
|
41
|
+
/\.(select|omit|sort)\s*\(\s*([\[\{]?[^\)]*)$/
|
|
42
|
+
const isInChainableMethodCall = chainableMethodCallRegex.test(before)
|
|
43
|
+
|
|
44
|
+
// Detect if we are inside a .select([]), .omit([]), .sort([]), etc. as a method call (e.g. User.find().select([]))
|
|
45
|
+
// This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
|
|
46
|
+
const chainableDirectCallMatch = before.match(
|
|
47
|
+
/\.(select|omit|sort|populate|where)\s*\(\s*\[?\s*['"]?([a-zA-Z0-9_]*)?$/
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Also allow completions in .where({ ... }) chainable method call context
|
|
51
|
+
const whereMethodCallRegex = /\.where\s*\(\s*\{[^\)]*$/
|
|
52
|
+
const isInWhereMethodCall = whereMethodCallRegex.test(before)
|
|
53
|
+
|
|
54
|
+
// Add: detect .where({ ... }) context for modelName/prefix inference
|
|
55
|
+
const whereMethodCallMatch = before.match(
|
|
56
|
+
/([A-Za-z_$][\w$]*)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// Only suppress completions after a colon (:) in object literals for static methods,
|
|
60
|
+
// but always allow completions in .select(['']), .omit(['']), .sort(['']), .where({}), etc.
|
|
61
|
+
const inChainableString =
|
|
62
|
+
selectStringMatch ||
|
|
63
|
+
selectArrayMatch ||
|
|
64
|
+
sortStringMatch ||
|
|
65
|
+
sortArrayStringMatch ||
|
|
66
|
+
sortArrayObjectMatch ||
|
|
67
|
+
populateStringMatch ||
|
|
68
|
+
isInChainableMethodCall ||
|
|
69
|
+
isInWhereMethodCall ||
|
|
70
|
+
!!chainableDirectCallMatch
|
|
71
|
+
|
|
72
|
+
// Suppress completions after a colon only if NOT in a chainable string/array context
|
|
73
|
+
if (!inChainableString) {
|
|
74
|
+
const lines = before.split('\n')
|
|
75
|
+
const line = lines[lines.length - 1]
|
|
76
|
+
const beforeCursor = line.slice(0, position.character)
|
|
77
|
+
// If the last non-whitespace character before the cursor is a colon, suppress completion
|
|
78
|
+
// (but allow after comma, or at start of line/object)
|
|
79
|
+
const lastColon = beforeCursor.lastIndexOf(':')
|
|
80
|
+
const lastComma = beforeCursor.lastIndexOf(',')
|
|
81
|
+
if (lastColon > lastComma && lastColon > beforeCursor.lastIndexOf('{')) {
|
|
82
|
+
// Check if we are inside a string (e.g. after a colon and inside quotes)
|
|
83
|
+
// If so, suppress completion
|
|
84
|
+
const quoteBefore = beforeCursor.lastIndexOf("'")
|
|
85
|
+
const dquoteBefore = beforeCursor.lastIndexOf('"')
|
|
86
|
+
if (
|
|
87
|
+
(quoteBefore > lastColon && quoteBefore > lastComma) ||
|
|
88
|
+
(dquoteBefore > lastColon && dquoteBefore > lastComma)
|
|
89
|
+
) {
|
|
90
|
+
return []
|
|
91
|
+
}
|
|
92
|
+
// Otherwise, suppress completion after colon
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
38
97
|
let modelName,
|
|
39
98
|
prefix,
|
|
40
99
|
attributes,
|
|
@@ -69,6 +128,12 @@ module.exports = function modelAttributesCompletion(
|
|
|
69
128
|
modelName = inferModelName(before)
|
|
70
129
|
prefix =
|
|
71
130
|
(sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch)[1] || ''
|
|
131
|
+
} else if (whereMethodCallMatch) {
|
|
132
|
+
modelName = whereMethodCallMatch[1]
|
|
133
|
+
prefix = whereMethodCallMatch[2] || ''
|
|
134
|
+
} else if (chainableDirectCallMatch) {
|
|
135
|
+
modelName = inferModelName(before)
|
|
136
|
+
prefix = chainableDirectCallMatch[2] || ''
|
|
72
137
|
} else {
|
|
73
138
|
return []
|
|
74
139
|
}
|
|
@@ -80,6 +145,27 @@ module.exports = function modelAttributesCompletion(
|
|
|
80
145
|
const model = foundKey ? models[foundKey] : null
|
|
81
146
|
if (!model) return []
|
|
82
147
|
|
|
148
|
+
// Find already-used property names in the current object literal (if inside one)
|
|
149
|
+
let usedProps = new Set()
|
|
150
|
+
// Use the text before the cursor to find the nearest opening brace
|
|
151
|
+
const beforeCursorFull = text.slice(0, offset)
|
|
152
|
+
const lastOpen = beforeCursorFull.lastIndexOf('{')
|
|
153
|
+
const lastClose = beforeCursorFull.lastIndexOf('}')
|
|
154
|
+
if (lastOpen !== -1 && (lastClose === -1 || lastOpen > lastClose)) {
|
|
155
|
+
// Only consider properties before the cursor
|
|
156
|
+
const objectText = beforeCursorFull.slice(lastOpen, offset)
|
|
157
|
+
// Match both foo: ... and object shorthand foo,
|
|
158
|
+
const propRegex = /([a-zA-Z0-9_]+)\s*:/g
|
|
159
|
+
const shorthandRegex = /([a-zA-Z0-9_]+)\s*,/g
|
|
160
|
+
let m
|
|
161
|
+
while ((m = propRegex.exec(objectText)) !== null) {
|
|
162
|
+
usedProps.add(m[1])
|
|
163
|
+
}
|
|
164
|
+
while ((m = shorthandRegex.exec(objectText)) !== null) {
|
|
165
|
+
usedProps.add(m[1])
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
83
169
|
if (isPopulate) {
|
|
84
170
|
attributes = Object.entries(model.attributes || {})
|
|
85
171
|
.filter(([, def]) => def && (def.model || def.collection))
|
|
@@ -90,13 +176,20 @@ module.exports = function modelAttributesCompletion(
|
|
|
90
176
|
|
|
91
177
|
return attributes
|
|
92
178
|
.filter((attr) => attr.startsWith(prefix))
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
179
|
+
.filter((attr) => !usedProps.has(attr))
|
|
180
|
+
.map((attr) => {
|
|
181
|
+
const attrDef = model.attributes && model.attributes[attr]
|
|
182
|
+
let type = attrDef && attrDef.type ? attrDef.type : ''
|
|
183
|
+
let required = attrDef && attrDef.required ? 'required' : 'optional'
|
|
184
|
+
let detail = type ? `${type} (${required})` : required
|
|
185
|
+
return {
|
|
186
|
+
label: attr,
|
|
187
|
+
kind: lsp.CompletionItemKind.Field,
|
|
188
|
+
detail,
|
|
189
|
+
documentation: `${modelName}.${attr}`,
|
|
190
|
+
sortText: attr,
|
|
191
|
+
filterText: attr,
|
|
192
|
+
insertText: attr
|
|
193
|
+
}
|
|
194
|
+
})
|
|
102
195
|
}
|
|
@@ -48,14 +48,24 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
|
|
|
48
48
|
|
|
49
49
|
return methods
|
|
50
50
|
.filter((method) => method.name.startsWith(prefix))
|
|
51
|
-
.map((method) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
.map((method) => {
|
|
52
|
+
let insertText = method.name + '($0)'
|
|
53
|
+
// For chainable .select or .omit, insert ([''])
|
|
54
|
+
if (
|
|
55
|
+
chainableCallMatch &&
|
|
56
|
+
(method.name === 'select' || method.name === 'omit')
|
|
57
|
+
) {
|
|
58
|
+
insertText = method.name + '([$0])'
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
label: method.name,
|
|
62
|
+
kind: lsp.CompletionItemKind.Method,
|
|
63
|
+
detail: method.description,
|
|
64
|
+
documentation: `${modelName}.${method.name}()`,
|
|
65
|
+
sortText: method.name,
|
|
66
|
+
filterText: method.name,
|
|
67
|
+
insertText,
|
|
68
|
+
insertTextFormat: lsp.InsertTextFormat.Snippet
|
|
69
|
+
}
|
|
70
|
+
})
|
|
61
71
|
}
|
|
@@ -8,40 +8,45 @@ module.exports = async function goToHelper(document, position, typeMap) {
|
|
|
8
8
|
const text = document.getText()
|
|
9
9
|
const offset = document.offsetAt(position)
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
14
14
|
|
|
15
15
|
let match
|
|
16
|
-
|
|
17
16
|
while ((match = regex.exec(text)) !== null) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
34
|
+
|
|
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)
|
|
42
46
|
)
|
|
43
|
-
|
|
47
|
+
)
|
|
44
48
|
}
|
|
45
49
|
}
|
|
50
|
+
|
|
46
51
|
return null
|
|
47
52
|
}
|
package/index.js
CHANGED
|
@@ -24,6 +24,9 @@ const policiesCompletion = require('./completions/policies-completion')
|
|
|
24
24
|
const viewsCompletion = require('./completions/views-completion')
|
|
25
25
|
const modelMethodsCompletion = require('./completions/model-methods-completion')
|
|
26
26
|
const modelAttributesCompletion = require('./completions/model-attributes-completion')
|
|
27
|
+
const helperInputsCompletion = require('./completions/helper-inputs-completion')
|
|
28
|
+
const helpersCompletion = require('./completions/helpers-completion')
|
|
29
|
+
|
|
27
30
|
const connection = lsp.createConnection(lsp.ProposedFeatures.all)
|
|
28
31
|
const documents = new lsp.TextDocuments(TextDocument)
|
|
29
32
|
|
|
@@ -116,7 +119,9 @@ connection.onCompletion(async (params) => {
|
|
|
116
119
|
policyCompletion,
|
|
117
120
|
viewCompletion,
|
|
118
121
|
modelMethodCompletion,
|
|
119
|
-
modelAttributeCompletion
|
|
122
|
+
modelAttributeCompletion,
|
|
123
|
+
helperCompletion,
|
|
124
|
+
helperInputCompletion
|
|
120
125
|
] = await Promise.all([
|
|
121
126
|
actionsCompletion(document, params.position, typeMap),
|
|
122
127
|
dataTypesCompletion(document, params.position, typeMap),
|
|
@@ -127,7 +132,9 @@ connection.onCompletion(async (params) => {
|
|
|
127
132
|
policiesCompletion(document, params.position, typeMap),
|
|
128
133
|
viewsCompletion(document, params.position, typeMap),
|
|
129
134
|
modelMethodsCompletion(document, params.position, typeMap),
|
|
130
|
-
modelAttributesCompletion(document, params.position, typeMap)
|
|
135
|
+
modelAttributesCompletion(document, params.position, typeMap),
|
|
136
|
+
helpersCompletion(document, params.position, typeMap),
|
|
137
|
+
helperInputsCompletion(document, params.position, typeMap)
|
|
131
138
|
])
|
|
132
139
|
|
|
133
140
|
const completions = [
|
|
@@ -140,7 +147,9 @@ connection.onCompletion(async (params) => {
|
|
|
140
147
|
...policyCompletion,
|
|
141
148
|
...viewCompletion,
|
|
142
149
|
...modelMethodCompletion,
|
|
143
|
-
...modelAttributeCompletion
|
|
150
|
+
...modelAttributeCompletion,
|
|
151
|
+
...helperCompletion,
|
|
152
|
+
...helperInputCompletion
|
|
144
153
|
].filter(Boolean)
|
|
145
154
|
|
|
146
155
|
if (completions) {
|
package/package.json
CHANGED
|
@@ -4,6 +4,12 @@ const validatePageExist = require('./validate-page-exist')
|
|
|
4
4
|
const validateDataTypes = require('./validate-data-type')
|
|
5
5
|
const validatePolicyExist = require('./validate-policy-exist')
|
|
6
6
|
const validateModelAttributeExist = require('./validate-model-attribute-exist')
|
|
7
|
+
const validateViewExist = require('./validate-view-exist')
|
|
8
|
+
const validateHelperInputExist = require('./validate-helper-input-exist')
|
|
9
|
+
const validateRequiredHelperInput = require('./validate-required-helper-input')
|
|
10
|
+
const validateRequiredModelAttribute = require('./validate-required-model-attribute')
|
|
11
|
+
const validateModelExist = require('./validate-model-exist')
|
|
12
|
+
|
|
7
13
|
module.exports = function validateDocument(connection, document, typeMap) {
|
|
8
14
|
const diagnostics = []
|
|
9
15
|
|
|
@@ -16,13 +22,29 @@ module.exports = function validateDocument(connection, document, typeMap) {
|
|
|
16
22
|
document,
|
|
17
23
|
typeMap
|
|
18
24
|
)
|
|
25
|
+
const viewDiagnostics = validateViewExist(document, typeMap)
|
|
26
|
+
const helperInputDiagnostics = validateHelperInputExist(document, typeMap)
|
|
27
|
+
const requiredHelperInputDiagnostics = validateRequiredHelperInput(
|
|
28
|
+
document,
|
|
29
|
+
typeMap
|
|
30
|
+
)
|
|
31
|
+
const requiredModelAttributeDiagnostics = validateRequiredModelAttribute(
|
|
32
|
+
document,
|
|
33
|
+
typeMap
|
|
34
|
+
)
|
|
35
|
+
const modelExistDiagnostics = validateModelExist(document, typeMap)
|
|
19
36
|
diagnostics.push(
|
|
20
37
|
...modelDiagnostics,
|
|
21
38
|
...actionDiagnostics,
|
|
22
39
|
...pageDiagnostics,
|
|
23
40
|
...dataTypeDiagnostics,
|
|
24
41
|
...policyDiagnostics,
|
|
25
|
-
...modelAttributeDiagnostics
|
|
42
|
+
...modelAttributeDiagnostics,
|
|
43
|
+
...viewDiagnostics,
|
|
44
|
+
...helperInputDiagnostics,
|
|
45
|
+
...requiredHelperInputDiagnostics,
|
|
46
|
+
...requiredModelAttributeDiagnostics,
|
|
47
|
+
...modelExistDiagnostics
|
|
26
48
|
)
|
|
27
49
|
|
|
28
50
|
connection.sendDiagnostics({ uri: document.uri, diagnostics })
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateHelperInputExist(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
const text = document.getText()
|
|
6
|
+
|
|
7
|
+
// Regex to match sails.helpers.foo.bar.with({ ... })
|
|
8
|
+
// Captures: 1) helper path, 2) object literal content
|
|
9
|
+
const regex = /sails\.helpers((?:\.[a-zA-Z0-9_]+)+)\.with\s*\(\s*\{([^}]*)\}/g
|
|
10
|
+
let match
|
|
11
|
+
while ((match = regex.exec(text)) !== null) {
|
|
12
|
+
// Build helper name: e.g. .foo.bar => foo/bar
|
|
13
|
+
const segments = match[1].split('.').filter(Boolean)
|
|
14
|
+
const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
15
|
+
const fullHelperName = segments.map(toKebab).join('/')
|
|
16
|
+
const helperInfo = typeMap.helpers && typeMap.helpers[fullHelperName]
|
|
17
|
+
if (!helperInfo || !helperInfo.inputs) continue
|
|
18
|
+
|
|
19
|
+
// Find all property names in the object literal
|
|
20
|
+
const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
|
|
21
|
+
let propMatch
|
|
22
|
+
while ((propMatch = propsRegex.exec(match[2])) !== null) {
|
|
23
|
+
const key = propMatch[1]
|
|
24
|
+
if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
|
|
25
|
+
const start = match.index + match[0].indexOf(key)
|
|
26
|
+
const end = start + key.length
|
|
27
|
+
diagnostics.push(
|
|
28
|
+
lsp.Diagnostic.create(
|
|
29
|
+
lsp.Range.create(
|
|
30
|
+
document.positionAt(start),
|
|
31
|
+
document.positionAt(end)
|
|
32
|
+
),
|
|
33
|
+
`Unknown input property '${key}' for helper '${fullHelperName}'.`,
|
|
34
|
+
lsp.DiagnosticSeverity.Error,
|
|
35
|
+
'sails-lsp'
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return diagnostics
|
|
42
|
+
}
|