@sailshq/language-server 0.0.3 → 0.0.4

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,63 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ const loadSails = require('../helpers/load-sails')
3
+
4
+ module.exports = async function sailsCompletions(document, position) {
5
+ const text = document.getText()
6
+ const offset = document.offsetAt(position)
7
+ const line = text.substring(0, offset).split('\n').pop()
8
+
9
+ const match = line.match(/sails((?:\.[a-zA-Z_$][0-9a-zA-Z_$]*)*)\.$/)
10
+ if (match) {
11
+ try {
12
+ return await loadSails(document.uri, (sailsApp) => {
13
+ const path = match[1].split('.').filter(Boolean)
14
+ return getNestedCompletions(sailsApp, path)
15
+ })
16
+ } catch (error) {
17
+ return []
18
+ }
19
+ }
20
+
21
+ return null
22
+ }
23
+
24
+ function getNestedCompletions(obj, path) {
25
+ let current = obj
26
+ for (const key of path) {
27
+ if (current && typeof current === 'object' && key in current) {
28
+ current = current[key]
29
+ } else {
30
+ return []
31
+ }
32
+ }
33
+
34
+ if (typeof current !== 'object' || current === null) {
35
+ return []
36
+ }
37
+
38
+ const completions = Object.keys(current).map((key) => {
39
+ const value = current[key]
40
+ let kind = lsp.CompletionItemKind.Property
41
+ let detail = 'Property'
42
+
43
+ if (typeof value === 'function') {
44
+ kind = lsp.CompletionItemKind.Method
45
+ detail = 'Method'
46
+ } else if (typeof value === 'object' && value !== null) {
47
+ detail = 'Object'
48
+ }
49
+
50
+ return {
51
+ label: key,
52
+ kind: kind,
53
+ detail: detail,
54
+ documentation: `Access to sails${path.length ? '.' + path.join('.') : ''}.${key}`,
55
+ sortText: key.startsWith('_') ? `z${key}` : key // Add this line
56
+ }
57
+ })
58
+
59
+ // Sort the completions
60
+ completions.sort((a, b) => a.sortText.localeCompare(b.sortText))
61
+
62
+ return completions
63
+ }
@@ -1,7 +1,6 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const path = require('path')
3
- const fs = require('fs').promises
4
- const url = require('url')
3
+ const findFnLine = require('../helpers/find-fn-line')
5
4
 
6
5
  module.exports = async function goToAction(document, position) {
7
6
  const fileName = path.basename(document.uri)
@@ -65,18 +64,3 @@ function extractActionInfo(document, position) {
65
64
  function resolveActionPath(projectRoot, actionPath) {
66
65
  return path.join(projectRoot, 'api', 'controllers', `${actionPath}.js`)
67
66
  }
68
-
69
- async function findFnLine(filePath) {
70
- try {
71
- const content = await fs.readFile(url.fileURLToPath(filePath), 'utf8')
72
- const lines = content.split('\n')
73
- for (let i = 0; i < lines.length; i++) {
74
- if (lines[i].includes('fn:')) {
75
- return i // Return the line number (0-based index)
76
- }
77
- }
78
- return 0 // If 'fn:' is not found, return the first line
79
- } catch (error) {
80
- return 0 // Return the first line if there's an error
81
- }
82
- }
@@ -0,0 +1,47 @@
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
+
7
+ module.exports = async function goToHelper(document, position) {
8
+ const helperInfo = extractHelperInfo(document, position)
9
+
10
+ if (!helperInfo) {
11
+ return null
12
+ }
13
+
14
+ const projectRoot = await findProjectRoot(document.uri)
15
+ const fullHelperPath =
16
+ path.join(projectRoot, 'api', 'helpers', ...helperInfo.helperPath) + '.js'
17
+
18
+ if (fullHelperPath) {
19
+ const fnLineNumber = await findFnLine(fullHelperPath)
20
+ return lsp.Location.create(
21
+ fullHelperPath,
22
+ lsp.Range.create(fnLineNumber, 0, fnLineNumber, 0)
23
+ )
24
+ }
25
+ }
26
+
27
+ function extractHelperInfo(document, position) {
28
+ const text = document.getText()
29
+ const offset = document.offsetAt(position)
30
+
31
+ // Regular expression to match sails.helpers.exampleHelper() or sails.helpers.exampleHelper.with()
32
+ // Also matches nested helpers like sails.helpers.mail.send() or sails.helpers.mail.send.with()
33
+ const regex = /sails\.helpers\.([a-zA-Z0-9.]+)(?:\.with)?\s*\(/g
34
+ let match
35
+
36
+ while ((match = regex.exec(text)) !== null) {
37
+ const start = match.index
38
+ const end = start + match[0].length
39
+
40
+ if (start <= offset && offset <= end) {
41
+ const helperPath = match[1].split('.').filter((part) => part !== 'with')
42
+ return { helperPath }
43
+ }
44
+ }
45
+
46
+ return null
47
+ }
@@ -0,0 +1,53 @@
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
+
6
+ module.exports = async function goToInertiaPage(document, position) {
7
+ const pageInfo = extractPageInfo(document, position)
8
+
9
+ if (!pageInfo) {
10
+ return null
11
+ }
12
+
13
+ const projectRoot = await findProjectRoot(document.uri)
14
+ const possibleExtensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'] // Add or remove extensions as needed
15
+
16
+ for (const ext of possibleExtensions) {
17
+ const fullPagePath = path.join(
18
+ projectRoot,
19
+ 'assets',
20
+ 'js',
21
+ 'pages',
22
+ `${pageInfo.page}${ext}`
23
+ )
24
+ try {
25
+ await fs.access(fullPagePath)
26
+ return lsp.Location.create(fullPagePath, lsp.Range.create(0, 0, 0, 0))
27
+ } catch (error) {
28
+ // File doesn't exist with this extension, try the next one
29
+ }
30
+ }
31
+
32
+ return null
33
+ }
34
+
35
+ function extractPageInfo(document, position) {
36
+ const text = document.getText()
37
+ const offset = document.offsetAt(position)
38
+
39
+ // Regular expression to match { page: 'example' } or { page: "example" }
40
+ const regex = /{\s*page\s*:\s*['"]([^'"]+)['"]\s*}/g
41
+ let match
42
+
43
+ while ((match = regex.exec(text)) !== null) {
44
+ const start = match.index
45
+ const end = start + match[0].length
46
+
47
+ if (start <= offset && offset <= end) {
48
+ return { page: match[1] }
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
@@ -0,0 +1,90 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ const path = require('path')
3
+ const fs = require('fs').promises
4
+
5
+ module.exports = async function goToPolicy(document, position) {
6
+ const fileName = path.basename(document.uri)
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
+ const text = document.getText()
30
+ const offset = document.offsetAt(position)
31
+
32
+ // This regex matches policy definitions, including arrays of policies and boolean values
33
+ const regex =
34
+ /(['"])((?:\*|[\w-]+(?:\/\*?)?))?\1\s*:\s*((?:\[?\s*(?:(?:['"][\w-]+['"](?:\s*,\s*)?)+)\s*\]?)|true|false)/g
35
+ let match
36
+
37
+ while ((match = regex.exec(text)) !== null) {
38
+ const [fullMatch, , route, policiesOrBoolean] = match
39
+ const start = match.index
40
+ const end = start + fullMatch.length
41
+
42
+ // Check if the cursor is anywhere within the entire match
43
+ if (start <= offset && offset <= end) {
44
+ // If policiesOrBoolean is a boolean, ignore it
45
+ if (policiesOrBoolean === true || policiesOrBoolean === false) {
46
+ continue
47
+ }
48
+
49
+ // Remove brackets if present and split into individual policies
50
+ const policies = policiesOrBoolean
51
+ .replace(/^\[|\]$/g, '')
52
+ .split(',')
53
+ .map((p) => p.trim().replace(/^['"]|['"]$/g, ''))
54
+
55
+ // Find which policy the cursor is on
56
+ let currentStart = start + fullMatch.indexOf(policiesOrBoolean)
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
72
+ }
73
+ }
74
+ }
75
+
76
+ return null
77
+ }
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
+ }
@@ -0,0 +1,70 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+ const path = require('path')
3
+ const fs = require('fs').promises
4
+
5
+ const findProjectRoot = require('../helpers/find-project-root')
6
+
7
+ module.exports = async function goToView(document, position) {
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) {
31
+ const text = document.getText()
32
+ const offset = document.offsetAt(position)
33
+
34
+ // This regex matches both object notation for views and viewTemplatePath
35
+ const regex =
36
+ /(?:(['"])(.+?)\1\s*:\s*{\s*view\s*:\s*(['"])(.+?)\3\s*}|viewTemplatePath\s*:\s*(['"])(.+?)\5)/g
37
+ let match
38
+
39
+ while ((match = regex.exec(text)) !== null) {
40
+ const [fullMatch, , , , viewInObject, , viewInController] = match
41
+ const view = viewInObject || viewInController
42
+ const start = match.index
43
+ const end = start + fullMatch.length
44
+
45
+ // Check if the cursor is anywhere within the entire match
46
+ if (start <= offset && offset <= end) {
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
54
+
55
+ return {
56
+ view,
57
+ range: lsp.Range.create(
58
+ document.positionAt(viewStartWithQuote),
59
+ document.positionAt(viewEndWithQuote)
60
+ )
61
+ }
62
+ }
63
+ }
64
+
65
+ return null
66
+ }
67
+
68
+ function resolveViewPath(projectRoot, viewPath) {
69
+ return path.join(projectRoot, 'views', `${viewPath}.ejs`)
70
+ }
@@ -0,0 +1,21 @@
1
+ const fs = require('fs').promises
2
+ const url = require('url')
3
+
4
+ module.exports = async function findFnLine(filePath) {
5
+ try {
6
+ const resolvedPath = filePath.startsWith('file:')
7
+ ? url.fileURLToPath(filePath)
8
+ : filePath
9
+
10
+ const content = await fs.readFile(resolvedPath, 'utf8')
11
+ const lines = content.split('\n')
12
+ for (let i = 0; i < lines.length; i++) {
13
+ if (lines[i].includes('fn:')) {
14
+ return i // Return the line number (0-based index)
15
+ }
16
+ }
17
+ return 0 // If 'fn:' is not found, return the first line
18
+ } catch (error) {
19
+ return 0 // Return the first line if there's an error
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ const path = require('path')
2
+ const url = require('url')
3
+ const fs = require('fs').promises
4
+
5
+ module.exports = async function findProjectRoot(uri) {
6
+ let currentPath = path.dirname(url.fileURLToPath(uri))
7
+ const root = path.parse(currentPath).root
8
+
9
+ while (currentPath !== root) {
10
+ try {
11
+ await fs.access(path.join(currentPath, 'package.json'))
12
+ return currentPath
13
+ } catch (error) {
14
+ currentPath = path.dirname(currentPath)
15
+ }
16
+ }
17
+ throw new Error('Could not find project root')
18
+ }
@@ -0,0 +1,12 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const findProjectRoot = require('./find-project-root')
4
+
5
+ module.exports = async function findSails(workspaceUri) {
6
+ const projectRoot = await findProjectRoot(workspaceUri)
7
+ const sailsPath = path.join(projectRoot, 'node_modules', 'sails')
8
+ if (fs.existsSync(sailsPath)) {
9
+ return { sailsPath, projectRoot }
10
+ }
11
+ throw new Error('Sails not found in node_modules')
12
+ }
@@ -0,0 +1,38 @@
1
+ const findSails = require('./find-sails')
2
+
3
+ module.exports = async function loadSails(workspaceUri, operation) {
4
+ let Sails
5
+ let sailsApp
6
+
7
+ try {
8
+ const { sailsPath } = await findSails(workspaceUri)
9
+ Sails = require(sailsPath).constructor
10
+
11
+ sailsApp = await new Promise((resolve, reject) => {
12
+ new Sails().load(
13
+ {
14
+ hooks: { shipwright: false },
15
+ log: { level: 'silent' }
16
+ },
17
+ (err, sails) => {
18
+ if (err) {
19
+ console.error('Failed to load Sails app:', err)
20
+ return reject(err)
21
+ }
22
+ resolve(sails)
23
+ }
24
+ )
25
+ })
26
+
27
+ // Execute the operation with the loaded Sails app
28
+ return await operation(sailsApp)
29
+ } catch (error) {
30
+ console.error('Error loading or working with Sails app:', error)
31
+ throw error
32
+ } finally {
33
+ // Ensure Sails is lowered even if an error occurred
34
+ if (sailsApp && typeof sailsApp.lower === 'function') {
35
+ await new Promise((resolve) => sailsApp.lower(resolve))
36
+ }
37
+ }
38
+ }
package/index.js CHANGED
@@ -1,7 +1,18 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
2
  const TextDocument = require('vscode-languageserver-textdocument').TextDocument
3
+
4
+ // Validators
3
5
  const validateDocument = require('./validators/validate-document')
6
+
7
+ // Go-to definitions
4
8
  const goToAction = require('./go-to-definitions/go-to-action')
9
+ const goToPolicy = require('./go-to-definitions/go-to-policy')
10
+ const goToView = require('./go-to-definitions/go-to-view')
11
+ const goToInertiaPage = require('./go-to-definitions/go-to-inertia-page')
12
+ const goToHelper = require('./go-to-definitions/go-to-helper')
13
+
14
+ // Completions
15
+ const sailsCompletions = require('./completions/sails-completions')
5
16
 
6
17
  const connection = lsp.createConnection(lsp.ProposedFeatures.all)
7
18
  const documents = new lsp.TextDocuments(TextDocument)
@@ -10,11 +21,10 @@ connection.onInitialize((params) => {
10
21
  return {
11
22
  capabilities: {
12
23
  textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
13
- definitionProvider: true
14
- // completionProvider: {
15
- // resolveProvider: true,
16
- // triggerCharacters: ['"', "'", '.']
17
- // }
24
+ definitionProvider: true,
25
+ completionProvider: {
26
+ triggerCharacters: ['"', "'", '.']
27
+ }
18
28
  }
19
29
  }
20
30
  })
@@ -32,11 +42,45 @@ connection.onDefinition(async (params) => {
32
42
  if (!document) {
33
43
  return null
34
44
  }
35
- const definitions = []
36
- const actionDefinitions = await goToAction(document, params.position)
37
- definitions.push(actionDefinitions)
38
- return definitions || null
45
+
46
+ const actionDefinition = await goToAction(document, params.position)
47
+ const policyDefinition = await goToPolicy(document, params.position)
48
+ const viewDefinition = await goToView(document, params.position)
49
+ const inertiaPageDefinition = await goToInertiaPage(document, params.position)
50
+ const helperDefinition = await goToHelper(document, params.position)
51
+
52
+ const definitions = [
53
+ actionDefinition,
54
+ policyDefinition,
55
+ viewDefinition,
56
+ inertiaPageDefinition,
57
+ helperDefinition
58
+ ].filter(Boolean)
59
+
60
+ return definitions.length > 0 ? definitions : null
61
+ })
62
+
63
+ connection.onCompletion(async (params) => {
64
+ const document = documents.get(params.textDocument.uri)
65
+ if (!document) {
66
+ return null
67
+ }
68
+
69
+ const completions = await sailsCompletions(document, params.position)
70
+
71
+ if (completions) {
72
+ return {
73
+ isIncomplete: false,
74
+ items: completions
75
+ }
76
+ }
77
+
78
+ return null
39
79
  })
40
80
 
41
81
  documents.listen(connection)
42
82
  connection.listen()
83
+
84
+ connection.console.log = (message) => {
85
+ console.log(message)
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -18,6 +18,10 @@ module.exports = function validateActionExist(document) {
18
18
  const actions = extractActionInfo(document) // Get all actions
19
19
 
20
20
  for (const { action, range } of actions) {
21
+ if (isUrlOrRedirect(action)) {
22
+ continue
23
+ }
24
+
21
25
  const fullActionPath = resolveActionPath(projectRoot, action)
22
26
  if (!fs.existsSync(url.fileURLToPath(fullActionPath))) {
23
27
  const diagnostic = {
@@ -63,3 +67,15 @@ function extractActionInfo(document) {
63
67
  function resolveActionPath(projectRoot, actionPath) {
64
68
  return path.join(projectRoot, 'api', 'controllers', `${actionPath}.js`)
65
69
  }
70
+
71
+ function isUrlOrRedirect(action) {
72
+ if (action.startsWith('http://') || action.startsWith('https://')) {
73
+ return true
74
+ }
75
+
76
+ if (action.startsWith('/')) {
77
+ return true
78
+ }
79
+
80
+ return false
81
+ }