@sailshq/language-server 0.4.0 → 0.5.1

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 CHANGED
@@ -229,7 +229,11 @@ class SailsParser {
229
229
  prop.key?.name === 'attributes' &&
230
230
  prop.value?.type === 'ObjectExpression'
231
231
  ) {
232
- defaultAttributes = context.#extractObjectLiteral(prop.value)
232
+ defaultAttributes = context.#extractObjectLiteral(
233
+ prop.value,
234
+ true,
235
+ configCode
236
+ )
233
237
  }
234
238
  if (
235
239
  prop.key?.name === 'models' &&
@@ -241,7 +245,9 @@ class SailsParser {
241
245
  inner.value?.type === 'ObjectExpression'
242
246
  ) {
243
247
  defaultAttributes = context.#extractObjectLiteral(
244
- inner.value
248
+ inner.value,
249
+ true,
250
+ configCode
245
251
  )
246
252
  }
247
253
  }
@@ -262,7 +268,11 @@ class SailsParser {
262
268
  prop.key?.name === 'attributes' &&
263
269
  prop.value?.type === 'ObjectExpression'
264
270
  ) {
265
- defaultAttributes = context.#extractObjectLiteral(prop.value)
271
+ defaultAttributes = context.#extractObjectLiteral(
272
+ prop.value,
273
+ true,
274
+ configCode
275
+ )
266
276
  }
267
277
  }
268
278
  }
@@ -279,6 +289,7 @@ class SailsParser {
279
289
  const name = file.slice(0, -3)
280
290
  const modelPath = path.join(dir, file)
281
291
  let attributes = {}
292
+ let attributesLine = 0
282
293
  const context = this
283
294
 
284
295
  try {
@@ -302,7 +313,12 @@ class SailsParser {
302
313
  prop.key?.name === 'attributes' &&
303
314
  prop.value?.type === 'ObjectExpression'
304
315
  ) {
305
- attributes = context.#extractObjectLiteral(prop.value)
316
+ attributes = context.#extractObjectLiteral(
317
+ prop.value,
318
+ true,
319
+ code
320
+ )
321
+ attributesLine = context.#getLineNumber(prop.key.start, code)
306
322
  }
307
323
  }
308
324
  }
@@ -315,7 +331,11 @@ class SailsParser {
315
331
  node.left.property.name === 'attributes' &&
316
332
  node.right.type === 'ObjectExpression'
317
333
  ) {
318
- attributes = context.#extractObjectLiteral(node.right)
334
+ attributes = context.#extractObjectLiteral(node.right, true, code)
335
+ attributesLine = context.#getLineNumber(
336
+ node.left.property.start,
337
+ code
338
+ )
319
339
  }
320
340
  },
321
341
  ExportDefaultDeclaration(node) {
@@ -325,7 +345,12 @@ class SailsParser {
325
345
  prop.key?.name === 'attributes' &&
326
346
  prop.value?.type === 'ObjectExpression'
327
347
  ) {
328
- attributes = context.#extractObjectLiteral(prop.value)
348
+ attributes = context.#extractObjectLiteral(
349
+ prop.value,
350
+ true,
351
+ code
352
+ )
353
+ attributesLine = context.#getLineNumber(prop.key.start, code)
329
354
  }
330
355
  }
331
356
  }
@@ -339,13 +364,22 @@ class SailsParser {
339
364
  // Merge defaultAttributes first, then model attributes (model overrides default)
340
365
  const mergedAttributes = {}
341
366
  for (const key of Object.keys(defaultAttributes)) {
342
- mergedAttributes[key] = defaultAttributes[key]
367
+ const attr = defaultAttributes[key]
368
+ mergedAttributes[key] = {
369
+ ...attr,
370
+ path: modelsConfigPath
371
+ }
343
372
  }
344
373
  for (const key of Object.keys(attributes)) {
345
- mergedAttributes[key] = attributes[key]
374
+ const attr = attributes[key]
375
+ mergedAttributes[key] = {
376
+ ...attr,
377
+ path: modelPath
378
+ }
346
379
  }
347
380
  models[name] = {
348
381
  path: modelPath,
382
+ attributesLine,
349
383
  methods: STATIC_METHODS,
350
384
  chainableMethods: CHAINABLE_METHODS,
351
385
  attributes: mergedAttributes
@@ -482,7 +516,11 @@ class SailsParser {
482
516
  prop.key.name === 'inputs' &&
483
517
  prop.value.type === 'ObjectExpression'
484
518
  ) {
485
- inputs = context.#extractObjectLiteral(prop.value)
519
+ inputs = context.#extractObjectLiteral(
520
+ prop.value,
521
+ true,
522
+ content
523
+ )
486
524
  }
487
525
  if (
488
526
  prop.key &&
@@ -513,7 +551,11 @@ class SailsParser {
513
551
  prop.key.name === 'inputs' &&
514
552
  prop.value.type === 'ObjectExpression'
515
553
  ) {
516
- inputs = context.#extractObjectLiteral(prop.value)
554
+ inputs = context.#extractObjectLiteral(
555
+ prop.value,
556
+ true,
557
+ content
558
+ )
517
559
  }
518
560
  if (
519
561
  prop.key &&
@@ -539,14 +581,13 @@ class SailsParser {
539
581
  const match = content.match(/inputs\s*:\s*\{([\s\S]*?)\n\s*\}/m)
540
582
  if (match) {
541
583
  try {
542
- // Try to parse as JS object
543
584
  const fakeObj = `({${match[1]}})`
544
585
  const ast = acorn.parse(fakeObj, { ecmaVersion: 'latest' })
545
586
  let obj = {}
546
587
  walk.simple(ast, {
547
588
  ObjectExpression(node) {
548
589
  if (!obj || Object.keys(obj).length === 0) {
549
- obj = context.#extractObjectLiteral(node)
590
+ obj = context.#extractObjectLiteral(node, true, content)
550
591
  }
551
592
  }
552
593
  })
@@ -574,7 +615,7 @@ class SailsParser {
574
615
  return helpers
575
616
  }
576
617
 
577
- #extractObjectLiteral(node) {
618
+ #extractObjectLiteral(node, withLineNumbers = false, sourceCode = null) {
578
619
  if (node.type !== 'ObjectExpression') return undefined
579
620
  const obj = {}
580
621
  for (const prop of node.properties) {
@@ -583,11 +624,15 @@ class SailsParser {
583
624
  prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
584
625
  let value
585
626
  if (prop.value.type === 'ObjectExpression') {
586
- value = this.#extractObjectLiteral(prop.value)
627
+ value = this.#extractObjectLiteral(
628
+ prop.value,
629
+ withLineNumbers,
630
+ sourceCode
631
+ )
587
632
  } else if (prop.value.type === 'ArrayExpression') {
588
633
  value = prop.value.elements.map((el) =>
589
634
  el.type === 'ObjectExpression'
590
- ? this.#extractObjectLiteral(el)
635
+ ? this.#extractObjectLiteral(el, withLineNumbers, sourceCode)
591
636
  : el.type === 'Literal'
592
637
  ? el.value
593
638
  : el.type === 'Identifier'
@@ -601,11 +646,20 @@ class SailsParser {
601
646
  } else {
602
647
  value = undefined
603
648
  }
604
- obj[key] = value
649
+ if (withLineNumbers && sourceCode) {
650
+ const line = this.#getLineNumber(prop.key.start, sourceCode)
651
+ obj[key] = { value, line }
652
+ } else {
653
+ obj[key] = value
654
+ }
605
655
  }
606
656
  }
607
657
  return obj
608
658
  }
659
+ #getLineNumber(offset, sourceCode) {
660
+ const lines = sourceCode.substring(0, offset).split('\n')
661
+ return lines.length
662
+ }
609
663
  #getDataTypes() {
610
664
  return [
611
665
  {
@@ -13,9 +13,14 @@ function getHelperPath(line) {
13
13
  function getInputCompletionItems(inputsObj) {
14
14
  if (!inputsObj || typeof inputsObj !== 'object') return []
15
15
  return Object.entries(inputsObj).map(([inputName, inputDef]) => {
16
- let type = inputDef?.type
17
- let required = inputDef?.required ? 'required' : 'optional'
18
- let description = inputDef?.description || ''
16
+ const typeValue = inputDef?.value?.type?.value ?? inputDef?.type
17
+ const requiredValue = inputDef?.value?.required?.value ?? inputDef?.required
18
+ const descriptionValue =
19
+ inputDef?.value?.description?.value ?? inputDef?.description
20
+
21
+ let type = typeValue
22
+ let required = requiredValue ? 'required' : 'optional'
23
+ let description = descriptionValue || ''
19
24
  let detail = type ? `${type} (${required})` : required
20
25
  return {
21
26
  label: inputName,
@@ -274,6 +274,7 @@ module.exports = function modelAttributesCompletion(
274
274
  let hasAttributes = false
275
275
  let hasQueryOptions = false
276
276
  let hasOperators = false
277
+ let hasModifiers = false
277
278
  for (const prop of objNode.properties) {
278
279
  if (!prop.key) continue
279
280
  const keyName = prop.key.name || prop.key.value
@@ -281,7 +282,9 @@ module.exports = function modelAttributesCompletion(
281
282
  hasQueryOptions = true
282
283
  } else if (WATERLINE_OPERATORS.includes(keyName)) {
283
284
  hasOperators = true
284
- } else if (!WATERLINE_MODIFIERS.includes(keyName)) {
285
+ } else if (WATERLINE_MODIFIERS.includes(keyName)) {
286
+ hasModifiers = true
287
+ } else {
285
288
  // Check if it's a valid attribute
286
289
  const model = getModelByName(astModelName)
287
290
  if (
@@ -303,12 +306,13 @@ module.exports = function modelAttributesCompletion(
303
306
  // - If we have query options (like where, select, limit), this is a query options object
304
307
  // - If we're explicitly inside a where clause (isInsideWhere), show attributes
305
308
  // - If we have attributes but no query options, it's a criteria object
309
+ // - Modifiers at top level don't make this a criteria object (only inside them)
306
310
  const isCriteriaMode =
307
311
  isInsideWhere || (hasAttributes && !hasQueryOptions)
308
312
 
309
- // If we have query options and we're not inside where, this is NOT a criteria context
313
+ // If we have query options or modifiers and we're not inside where, this is NOT a criteria context
310
314
  // Don't show attribute completions at the query options level
311
- if (hasQueryOptions && !isInsideWhere) {
315
+ if ((hasQueryOptions || hasModifiers) && !isInsideWhere) {
312
316
  // But we still need to check if we're typing a new key after existing query options
313
317
  // Check if cursor is in a position to type a new key
314
318
  const isTypingNewKey =
@@ -352,7 +356,7 @@ module.exports = function modelAttributesCompletion(
352
356
  }
353
357
  }
354
358
 
355
- // If it's a modifier (or/and/not), check inside the array
359
+ // If it's a modifier (or/and/not), check inside the array or object at any level
356
360
  if (WATERLINE_MODIFIERS.includes(keyName)) {
357
361
  if (
358
362
  prop.value &&
@@ -364,6 +368,8 @@ module.exports = function modelAttributesCompletion(
364
368
  if (checkObjectForCursor(el, true)) return true
365
369
  }
366
370
  }
371
+ } else if (prop.value && prop.value.type === 'ObjectExpression') {
372
+ if (checkObjectForCursor(prop.value, true)) return true
367
373
  }
368
374
  }
369
375
 
@@ -0,0 +1,112 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
4
+
5
+ module.exports = async function goToHelperInput(document, position, typeMap) {
6
+ const text = document.getText()
7
+ const offset = document.offsetAt(position)
8
+
9
+ try {
10
+ const ast = acorn.parse(text, {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module'
13
+ })
14
+
15
+ let result = null
16
+
17
+ walk.simple(ast, {
18
+ CallExpression(node) {
19
+ if (
20
+ node.callee &&
21
+ node.callee.type === 'MemberExpression' &&
22
+ node.callee.property.name === 'with' &&
23
+ node.callee.object &&
24
+ node.callee.object.type === 'MemberExpression'
25
+ ) {
26
+ const helperPath = extractHelperPath(node.callee.object)
27
+ if (!helperPath) return
28
+
29
+ const helperInfo = typeMap.helpers && typeMap.helpers[helperPath]
30
+ if (!helperInfo || !helperInfo.inputs) return
31
+
32
+ const objArg = node.arguments[0]
33
+ if (!objArg || objArg.type !== 'ObjectExpression') return
34
+
35
+ for (const prop of objArg.properties) {
36
+ if (prop.type !== 'Property' || !prop.key) continue
37
+
38
+ const inputName =
39
+ prop.key.type === 'Identifier'
40
+ ? prop.key.name
41
+ : prop.key.type === 'Literal'
42
+ ? prop.key.value
43
+ : null
44
+
45
+ if (!inputName) continue
46
+
47
+ const keyStart = prop.key.start
48
+ const keyEnd = prop.key.end
49
+
50
+ if (offset >= keyStart && offset <= keyEnd) {
51
+ const inputInfo = helperInfo.inputs[inputName]
52
+ if (inputInfo?.line) {
53
+ const uri = `file://${helperInfo.path}`
54
+ result = lsp.LocationLink.create(
55
+ uri,
56
+ lsp.Range.create(
57
+ inputInfo.line - 1,
58
+ 0,
59
+ inputInfo.line - 1,
60
+ 0
61
+ ),
62
+ lsp.Range.create(
63
+ inputInfo.line - 1,
64
+ 0,
65
+ inputInfo.line - 1,
66
+ 0
67
+ ),
68
+ lsp.Range.create(
69
+ document.positionAt(keyStart),
70
+ document.positionAt(keyEnd)
71
+ )
72
+ )
73
+ return
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ })
80
+
81
+ return result
82
+ } catch (error) {
83
+ return null
84
+ }
85
+ }
86
+
87
+ function extractHelperPath(node) {
88
+ const segments = []
89
+ let current = node
90
+
91
+ while (current && current.type === 'MemberExpression') {
92
+ if (current.property && current.property.type === 'Identifier') {
93
+ const propName = current.property.name
94
+ if (propName === 'helpers') {
95
+ if (
96
+ current.object &&
97
+ current.object.type === 'Identifier' &&
98
+ current.object.name === 'sails'
99
+ ) {
100
+ const toKebab = (s) =>
101
+ s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
102
+ return segments.map(toKebab).join('/')
103
+ }
104
+ return null
105
+ }
106
+ segments.unshift(propName)
107
+ }
108
+ current = current.object
109
+ }
110
+
111
+ return null
112
+ }
@@ -0,0 +1,302 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
4
+
5
+ module.exports = async function goToModelAttribute(
6
+ document,
7
+ position,
8
+ typeMap
9
+ ) {
10
+ const text = document.getText()
11
+ const offset = document.offsetAt(position)
12
+
13
+ try {
14
+ const ast = acorn.parse(text, {
15
+ ecmaVersion: 'latest',
16
+ sourceType: 'module'
17
+ })
18
+
19
+ let result = null
20
+
21
+ const checkPropertyForAttribute = (prop, model, document) => {
22
+ if (prop.type !== 'Property' || !prop.key) return null
23
+
24
+ const attrName =
25
+ prop.key.type === 'Identifier'
26
+ ? prop.key.name
27
+ : prop.key.type === 'Literal'
28
+ ? prop.key.value
29
+ : null
30
+
31
+ if (!attrName) return null
32
+
33
+ const keyStart = prop.key.start
34
+ const keyEnd = prop.key.end
35
+
36
+ if (offset >= keyStart && offset <= keyEnd) {
37
+ const attrInfo = model.attributes?.[attrName]
38
+ if (attrInfo?.line && attrInfo?.path) {
39
+ const uri = `file://${attrInfo.path}`
40
+ return lsp.LocationLink.create(
41
+ uri,
42
+ lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
43
+ lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
44
+ lsp.Range.create(
45
+ document.positionAt(keyStart),
46
+ document.positionAt(keyEnd)
47
+ )
48
+ )
49
+ }
50
+ }
51
+ return null
52
+ }
53
+
54
+ const checkStringLiteralForAttribute = (literal, model, document) => {
55
+ if (literal.type !== 'Literal' || typeof literal.value !== 'string')
56
+ return null
57
+
58
+ const attrName = literal.value
59
+ const literalStart = literal.start
60
+ const literalEnd = literal.end
61
+
62
+ if (offset >= literalStart && offset <= literalEnd) {
63
+ const attrInfo = model.attributes?.[attrName]
64
+ if (attrInfo?.line && attrInfo?.path) {
65
+ const uri = `file://${attrInfo.path}`
66
+ return lsp.LocationLink.create(
67
+ uri,
68
+ lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
69
+ lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
70
+ lsp.Range.create(
71
+ document.positionAt(literalStart),
72
+ document.positionAt(literalEnd)
73
+ )
74
+ )
75
+ }
76
+ }
77
+ return null
78
+ }
79
+
80
+ const checkArrayForAttributes = (arrayNode, model, document) => {
81
+ if (!arrayNode || arrayNode.type !== 'ArrayExpression') return null
82
+
83
+ for (const element of arrayNode.elements) {
84
+ if (element?.type === 'Literal' && typeof element.value === 'string') {
85
+ const res = checkStringLiteralForAttribute(element, model, document)
86
+ if (res) return res
87
+ } else if (element?.type === 'ObjectExpression') {
88
+ const res = traverseObjectExpression(element, model, document)
89
+ if (res) return res
90
+ }
91
+ }
92
+ return null
93
+ }
94
+
95
+ const traverseObjectExpression = (objNode, model, document) => {
96
+ if (!objNode || objNode.type !== 'ObjectExpression') return null
97
+
98
+ for (const prop of objNode.properties) {
99
+ if (prop.type !== 'Property') continue
100
+
101
+ const propName =
102
+ prop.key.type === 'Identifier'
103
+ ? prop.key.name
104
+ : prop.key.type === 'Literal'
105
+ ? prop.key.value
106
+ : null
107
+
108
+ if (
109
+ propName === 'where' ||
110
+ propName === 'or' ||
111
+ propName === 'and' ||
112
+ propName === 'not'
113
+ ) {
114
+ if (prop.value.type === 'ObjectExpression') {
115
+ const res = traverseObjectExpression(prop.value, model, document)
116
+ if (res) return res
117
+ } else if (prop.value.type === 'ArrayExpression') {
118
+ for (const element of prop.value.elements) {
119
+ const res = traverseObjectExpression(element, model, document)
120
+ if (res) return res
121
+ }
122
+ }
123
+ } else if (propName === 'select' || propName === 'omit') {
124
+ if (prop.value.type === 'ArrayExpression') {
125
+ const res = checkArrayForAttributes(prop.value, model, document)
126
+ if (res) return res
127
+ }
128
+ } else if (propName === 'sort') {
129
+ if (prop.value.type === 'ArrayExpression') {
130
+ const res = checkArrayForAttributes(prop.value, model, document)
131
+ if (res) return res
132
+ } else if (
133
+ prop.value.type === 'Literal' &&
134
+ typeof prop.value.value === 'string'
135
+ ) {
136
+ const sortString = prop.value.value
137
+ const attrMatch = sortString.match(/^(\w+)/)
138
+ if (attrMatch) {
139
+ const attrName = attrMatch[1]
140
+ const attrInfo = model.attributes?.[attrName]
141
+ if (attrInfo?.line && attrInfo?.path) {
142
+ const sortStart = prop.value.start
143
+ const sortEnd = prop.value.end
144
+ if (offset >= sortStart && offset <= sortEnd) {
145
+ const uri = `file://${attrInfo.path}`
146
+ return lsp.LocationLink.create(
147
+ uri,
148
+ lsp.Range.create(
149
+ attrInfo.line - 1,
150
+ 0,
151
+ attrInfo.line - 1,
152
+ 0
153
+ ),
154
+ lsp.Range.create(
155
+ attrInfo.line - 1,
156
+ 0,
157
+ attrInfo.line - 1,
158
+ 0
159
+ ),
160
+ lsp.Range.create(
161
+ document.positionAt(sortStart),
162
+ document.positionAt(sortEnd)
163
+ )
164
+ )
165
+ }
166
+ }
167
+ }
168
+ }
169
+ } else {
170
+ const res = checkPropertyForAttribute(prop, model, document)
171
+ if (res) return res
172
+
173
+ if (prop.value.type === 'ObjectExpression') {
174
+ const nestedRes = traverseObjectExpression(
175
+ prop.value,
176
+ model,
177
+ document
178
+ )
179
+ if (nestedRes) return nestedRes
180
+ }
181
+ }
182
+ }
183
+ return null
184
+ }
185
+
186
+ const findModelInChain = (node) => {
187
+ let current = node
188
+ while (current) {
189
+ if (
190
+ current.type === 'CallExpression' &&
191
+ current.callee?.type === 'MemberExpression'
192
+ ) {
193
+ if (current.callee.object?.type === 'Identifier') {
194
+ const modelName = current.callee.object.name
195
+ return typeMap.models?.[modelName]
196
+ }
197
+ current = current.callee.object
198
+ } else if (current.type === 'MemberExpression') {
199
+ current = current.object
200
+ } else {
201
+ break
202
+ }
203
+ }
204
+ return null
205
+ }
206
+
207
+ walk.simple(ast, {
208
+ CallExpression(node) {
209
+ if (node.callee.type === 'MemberExpression') {
210
+ const methodName =
211
+ node.callee.property?.type === 'Identifier'
212
+ ? node.callee.property.name
213
+ : null
214
+
215
+ if (methodName === 'select' || methodName === 'omit') {
216
+ const model = findModelInChain(node.callee.object)
217
+ if (model) {
218
+ const firstArg = node.arguments?.[0]
219
+ if (firstArg?.type === 'ArrayExpression') {
220
+ for (const element of firstArg.elements) {
221
+ const res = checkStringLiteralForAttribute(
222
+ element,
223
+ model,
224
+ document
225
+ )
226
+ if (res) {
227
+ result = res
228
+ return
229
+ }
230
+ }
231
+ }
232
+ }
233
+ } else if (methodName === 'sort') {
234
+ const model = findModelInChain(node.callee.object)
235
+ if (model) {
236
+ const firstArg = node.arguments?.[0]
237
+ if (firstArg?.type === 'ArrayExpression') {
238
+ const res = checkArrayForAttributes(firstArg, model, document)
239
+ if (res) {
240
+ result = res
241
+ return
242
+ }
243
+ } else if (
244
+ firstArg?.type === 'Literal' &&
245
+ typeof firstArg.value === 'string'
246
+ ) {
247
+ const sortString = firstArg.value
248
+ const attrMatch = sortString.match(/^(\w+)/)
249
+ if (attrMatch) {
250
+ const attrName = attrMatch[1]
251
+ const attrInfo = model.attributes?.[attrName]
252
+ if (attrInfo?.line && attrInfo?.path) {
253
+ const sortStart = firstArg.start
254
+ const sortEnd = firstArg.end
255
+ if (offset >= sortStart && offset <= sortEnd) {
256
+ const uri = `file://${attrInfo.path}`
257
+ result = lsp.LocationLink.create(
258
+ uri,
259
+ lsp.Range.create(
260
+ attrInfo.line - 1,
261
+ 0,
262
+ attrInfo.line - 1,
263
+ 0
264
+ ),
265
+ lsp.Range.create(
266
+ attrInfo.line - 1,
267
+ 0,
268
+ attrInfo.line - 1,
269
+ 0
270
+ ),
271
+ lsp.Range.create(
272
+ document.positionAt(sortStart),
273
+ document.positionAt(sortEnd)
274
+ )
275
+ )
276
+ return
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ } else if (node.callee.object.type === 'Identifier') {
283
+ const modelName = node.callee.object.name
284
+ const model = typeMap.models?.[modelName]
285
+
286
+ if (!model) return
287
+
288
+ const firstArg = node.arguments?.[0]
289
+ if (firstArg?.type === 'ObjectExpression') {
290
+ const res = traverseObjectExpression(firstArg, model, document)
291
+ if (res) result = res
292
+ }
293
+ }
294
+ }
295
+ }
296
+ })
297
+
298
+ return result
299
+ } catch (error) {
300
+ return null
301
+ }
302
+ }
@@ -25,12 +25,13 @@ module.exports = async function goToModel(document, position, typeMap) {
25
25
  const model = typeMap.models?.[modelName]
26
26
  if (!model?.path) return null
27
27
 
28
+ const targetLine = model.attributesLine ? model.attributesLine - 1 : 0
28
29
  const uri = `file://${model.path}`
29
30
  return lsp.LocationLink.create(
30
31
  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
32
+ lsp.Range.create(targetLine, 0, targetLine, 0),
33
+ lsp.Range.create(targetLine, 0, targetLine, 0),
34
+ lsp.Range.create(document.positionAt(start), document.positionAt(end))
34
35
  )
35
36
  }
36
37
  }
package/index.js CHANGED
@@ -11,7 +11,9 @@ const goToView = require('./go-to-definitions/go-to-view')
11
11
  const goToPage = require('./go-to-definitions/go-to-page')
12
12
  const goToPolicy = require('./go-to-definitions/go-to-policy')
13
13
  const goToHelper = require('./go-to-definitions/go-to-helper')
14
+ const goToHelperInput = require('./go-to-definitions/go-to-helper-input')
14
15
  const goToModel = require('./go-to-definitions/go-to-model')
16
+ const goToModelAttribute = require('./go-to-definitions/go-to-model-attribute')
15
17
 
16
18
  // Completions
17
19
  const actionsCompletion = require('./completions/actions-completion')
@@ -83,14 +85,18 @@ connection.onDefinition(async (params) => {
83
85
  pageDefinition,
84
86
  policyDefinition,
85
87
  helperDefinition,
86
- modelDefinition
88
+ helperInputDefinition,
89
+ modelDefinition,
90
+ modelAttributeDefinition
87
91
  ] = await Promise.all([
88
92
  goToAction(document, params.position, typeMap),
89
93
  goToView(document, params.position, typeMap),
90
94
  goToPage(document, params.position, typeMap),
91
95
  goToPolicy(document, params.position, typeMap),
92
96
  goToHelper(document, params.position, typeMap),
93
- goToModel(document, params.position, typeMap)
97
+ goToHelperInput(document, params.position, typeMap),
98
+ goToModel(document, params.position, typeMap),
99
+ goToModelAttribute(document, params.position, typeMap)
94
100
  ])
95
101
 
96
102
  const definitions = [
@@ -99,7 +105,9 @@ connection.onDefinition(async (params) => {
99
105
  pageDefinition,
100
106
  policyDefinition,
101
107
  helperDefinition,
102
- modelDefinition
108
+ helperInputDefinition,
109
+ modelDefinition,
110
+ modelAttributeDefinition
103
111
  ].filter((def) => def && (Array.isArray(def) ? def.length > 0 : true))
104
112
  return definitions
105
113
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -61,6 +61,14 @@ function validateCriteriaAttributes(
61
61
  )
62
62
  }
63
63
  }
64
+ } else if (prop.value && prop.value.type === 'ObjectExpression') {
65
+ validateCriteriaAttributes(
66
+ prop.value,
67
+ model,
68
+ document,
69
+ diagnostics,
70
+ effectiveModelName
71
+ )
64
72
  }
65
73
  continue
66
74
  }
@@ -455,6 +463,23 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
455
463
  effectiveModelName
456
464
  )
457
465
  continue
466
+ } else if (
467
+ WATERLINE_MODIFIERS.includes(attribute) &&
468
+ prop.value &&
469
+ prop.value.type === 'ArrayExpression'
470
+ ) {
471
+ for (const el of prop.value.elements) {
472
+ if (el && el.type === 'ObjectExpression') {
473
+ validateCriteriaAttributes(
474
+ el,
475
+ model,
476
+ document,
477
+ diagnostics,
478
+ effectiveModelName
479
+ )
480
+ }
481
+ }
482
+ continue
458
483
  }
459
484
  if (
460
485
  (attribute === 'select' || attribute === 'omit') &&
@@ -8,16 +8,18 @@ const lsp = require('vscode-languageserver/node')
8
8
  module.exports = function validateModelExist(document, typeMap) {
9
9
  const diagnostics = []
10
10
  const text = document.getText()
11
- // Build a lowercased model map for robust case-insensitive lookup
12
- const modelMap = {}
13
- if (typeMap.models) {
14
- for (const key of Object.keys(typeMap.models)) {
15
- modelMap[key.toLowerCase()] = typeMap.models[key]
16
- }
11
+ const models = typeMap.models || {}
12
+ const lowercasedModelMap = {}
13
+ for (const key of Object.keys(models)) {
14
+ lowercasedModelMap[key.toLowerCase()] = models[key]
17
15
  }
18
16
  function modelExists(name) {
19
17
  if (!name) return false
20
- return !!modelMap[name.toLowerCase()]
18
+ return !!models[name]
19
+ }
20
+ function modelExistsLowercased(name) {
21
+ if (!name) return false
22
+ return !!lowercasedModelMap[name.toLowerCase()]
21
23
  }
22
24
 
23
25
  const knownGlobals = [
@@ -30,9 +32,9 @@ module.exports = function validateModelExist(document, typeMap) {
30
32
  'process'
31
33
  ]
32
34
 
33
- // User.find() or User.create() etc
35
+ // User.find() or User.create() etc (only PascalCase identifiers)
34
36
  const modelCallRegex =
35
- /\b([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
37
+ /\b([A-Z][A-Za-z0-9_]*)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
36
38
  let match
37
39
  while ((match = modelCallRegex.exec(text)) !== null) {
38
40
  const modelName = match[1]
@@ -58,7 +60,7 @@ module.exports = function validateModelExist(document, typeMap) {
58
60
  /sails\.models\.([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
59
61
  while ((match = sailsModelCallRegex.exec(text)) !== null) {
60
62
  const modelName = match[1]
61
- if (!modelExists(modelName)) {
63
+ if (!modelExistsLowercased(modelName)) {
62
64
  diagnostics.push(
63
65
  lsp.Diagnostic.create(
64
66
  lsp.Range.create(
@@ -49,9 +49,10 @@ module.exports = function validateRequiredHelperInput(document, typeMap) {
49
49
  for (const [inputKey, inputDef] of Object.entries(
50
50
  helperInfo.inputs
51
51
  )) {
52
+ const requiredValue =
53
+ inputDef?.value?.required?.value ?? inputDef?.required
52
54
  const isRequired =
53
- inputDef &&
54
- (inputDef.required === true || inputDef.required === 'true')
55
+ requiredValue === true || requiredValue === 'true'
55
56
  if (isRequired && !providedKeys.has(inputKey)) {
56
57
  diagnostics.push(
57
58
  lsp.Diagnostic.create(