@sailshq/language-server 0.3.2 → 0.5.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.
@@ -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
  }
@@ -1,38 +1,52 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
4
+
2
5
  module.exports = async function goToPage(document, position, typeMap) {
3
6
  const filePath = document.uri
4
7
  if (!filePath.includes('/api/controllers/')) return null
5
8
  const text = document.getText()
6
9
  const offset = document.offsetAt(position)
7
10
 
8
- const regex =
9
- /{[^}]*?\bpage\s*:\s*(?<quote>['"])(?<page>[^'"]+)\k<quote>[^}]*?}/g
10
-
11
- let match
11
+ try {
12
+ const ast = acorn.parse(text, {
13
+ ecmaVersion: 'latest',
14
+ sourceType: 'module'
15
+ })
12
16
 
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
17
+ let result = null
19
18
 
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
- )
19
+ walk.simple(ast, {
20
+ Property(node) {
21
+ if (
22
+ node.key &&
23
+ (node.key.name === 'page' || node.key.value === 'page') &&
24
+ node.value &&
25
+ node.value.type === 'Literal' &&
26
+ typeof node.value.value === 'string'
27
+ ) {
28
+ const pageName = node.value.value
29
+ if (offset >= node.value.start && offset <= node.value.end) {
30
+ const pagePath = typeMap.pages?.[pageName]
31
+ if (pagePath) {
32
+ const uri = `file://${pagePath.path}`
33
+ result = lsp.LocationLink.create(
34
+ uri,
35
+ lsp.Range.create(0, 0, 0, 0),
36
+ lsp.Range.create(0, 0, 0, 0),
37
+ lsp.Range.create(
38
+ document.positionAt(node.value.start),
39
+ document.positionAt(node.value.end)
40
+ )
41
+ )
42
+ }
43
+ }
44
+ }
33
45
  }
34
- }
35
- }
46
+ })
36
47
 
37
- return null
48
+ return result
49
+ } catch (error) {
50
+ return null
51
+ }
38
52
  }
@@ -1,5 +1,7 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
+ const acorn = require('acorn')
4
+ const walk = require('acorn-walk')
3
5
 
4
6
  module.exports = async function goToView(document, position, typeMap) {
5
7
  const fileName = path.basename(document.uri)
@@ -12,32 +14,48 @@ module.exports = async function goToView(document, position, typeMap) {
12
14
 
13
15
  if (!isRoutes && !isController) return null
14
16
 
15
- const regex =
16
- /\b(viewTemplatePath|view)\s*:\s*(?<quote>['"])(?<view>[^'"]+)\k<quote>/g
17
+ try {
18
+ const ast = acorn.parse(text, {
19
+ ecmaVersion: 'latest',
20
+ sourceType: 'module'
21
+ })
17
22
 
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
23
+ let result = null
25
24
 
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
- )
38
- )
25
+ walk.simple(ast, {
26
+ Property(node) {
27
+ if (
28
+ node.key &&
29
+ (node.key.name === 'viewTemplatePath' ||
30
+ node.key.value === 'viewTemplatePath' ||
31
+ node.key.name === 'view' ||
32
+ node.key.value === 'view') &&
33
+ node.value &&
34
+ node.value.type === 'Literal' &&
35
+ typeof node.value.value === 'string'
36
+ ) {
37
+ const viewName = node.value.value
38
+ if (offset >= node.value.start && offset <= node.value.end) {
39
+ const viewPath = typeMap.views?.[viewName]
40
+ if (viewPath) {
41
+ const uri = `file://${viewPath.path}`
42
+ result = lsp.LocationLink.create(
43
+ uri,
44
+ lsp.Range.create(0, 0, 0, 0),
45
+ lsp.Range.create(0, 0, 0, 0),
46
+ lsp.Range.create(
47
+ document.positionAt(node.value.start),
48
+ document.positionAt(node.value.end)
49
+ )
50
+ )
51
+ }
52
+ }
53
+ }
39
54
  }
40
- }
55
+ })
56
+
57
+ return result
58
+ } catch (error) {
59
+ return null
41
60
  }
42
- return null
43
61
  }
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,8 +105,10 @@ connection.onDefinition(async (params) => {
99
105
  pageDefinition,
100
106
  policyDefinition,
101
107
  helperDefinition,
102
- modelDefinition
103
- ].filter(Boolean)
108
+ helperInputDefinition,
109
+ modelDefinition,
110
+ modelAttributeDefinition
111
+ ].filter((def) => def && (Array.isArray(def) ? def.length > 0 : true))
104
112
  return definitions
105
113
  })
106
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -1,4 +1,6 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
2
4
 
3
5
  module.exports = function validateActionExist(document, typeMap) {
4
6
  const diagnostics = []
@@ -28,23 +30,60 @@ module.exports = function validateActionExist(document, typeMap) {
28
30
 
29
31
  function extractActionInfo(document) {
30
32
  const text = document.getText()
31
- const regex = /(['"])(.+?)\1\s*:\s*(?:{?\s*action\s*:\s*)?(['"])(.+?)\3/g
32
33
  const actions = []
33
- let match
34
-
35
- while ((match = regex.exec(text)) !== null) {
36
- const action = match[4]
37
- const actionStart = match.index + match[0].lastIndexOf(action)
38
- const actionEnd = actionStart + action.length
39
-
40
- actions.push({
41
- action,
42
- range: lsp.Range.create(
43
- document.positionAt(actionStart),
44
- document.positionAt(actionEnd)
45
- )
34
+
35
+ try {
36
+ const ast = acorn.parse(text, {
37
+ ecmaVersion: 'latest',
38
+ sourceType: 'module'
46
39
  })
47
- }
40
+
41
+ walk.simple(ast, {
42
+ Property(node) {
43
+ const propertyKey = node.key?.value || node.key?.name
44
+
45
+ if (
46
+ propertyKey === 'action' &&
47
+ node.value?.type === 'Literal' &&
48
+ typeof node.value.value === 'string'
49
+ ) {
50
+ const actionName = node.value.value
51
+ actions.push({
52
+ action: actionName,
53
+ range: lsp.Range.create(
54
+ document.positionAt(node.value.start),
55
+ document.positionAt(node.value.end)
56
+ )
57
+ })
58
+ } else if (
59
+ typeof propertyKey === 'string' &&
60
+ (propertyKey.includes('GET') ||
61
+ propertyKey.includes('POST') ||
62
+ propertyKey.includes('PUT') ||
63
+ propertyKey.includes('PATCH') ||
64
+ propertyKey.includes('DELETE') ||
65
+ propertyKey.includes('/')) &&
66
+ node.value?.type === 'Literal' &&
67
+ typeof node.value.value === 'string'
68
+ ) {
69
+ const actionName = node.value.value
70
+ if (
71
+ !actionName.startsWith('/') &&
72
+ !actionName.startsWith('http://') &&
73
+ !actionName.startsWith('https://')
74
+ ) {
75
+ actions.push({
76
+ action: actionName,
77
+ range: lsp.Range.create(
78
+ document.positionAt(node.value.start),
79
+ document.positionAt(node.value.end)
80
+ )
81
+ })
82
+ }
83
+ }
84
+ }
85
+ })
86
+ } catch (error) {}
48
87
 
49
88
  return actions
50
89
  }