Skip to content

Commit 66fd94e

Browse files
committed
Add utilities for supporting sourcing
1 parent 18cfd87 commit 66fd94e

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { homedir } from 'os'
2+
3+
import { getSourcedUris } from '../sourcing'
4+
5+
const fileDirectory = '/Users/bash'
6+
const fileUri = `${fileDirectory}/file.sh`
7+
8+
describe('getSourcedUris', () => {
9+
it('returns an empty set if no files were sourced', () => {
10+
const result = getSourcedUris({ fileContent: '', fileUri })
11+
expect(result).toEqual(new Set([]))
12+
})
13+
14+
it('returns an empty set if no files were sourced', () => {
15+
const result = getSourcedUris({
16+
fileContent: `
17+
18+
source file-in-path.sh # does not contain a slash (i.e. is maybe somewhere on the path)
19+
20+
source /bin/f.inc
21+
22+
source ./x a b c # some arguments
23+
24+
. ./relative/to-this.sh
25+
26+
source ~/myscript
27+
28+
# source ...
29+
`,
30+
fileUri,
31+
})
32+
expect(result).toEqual(
33+
new Set([
34+
`${fileDirectory}/file-in-path.sh`, // as we don't resolve it, we hope it is here
35+
`${fileDirectory}/bin/f.inc`,
36+
`${fileDirectory}/x`,
37+
`${fileDirectory}/relative/to-this.sh`,
38+
`${homedir()}/myscript`,
39+
]),
40+
)
41+
})
42+
})

server/src/util/sourcing.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as path from 'path'
2+
import * as LSP from 'vscode-languageserver'
3+
import * as Parser from 'web-tree-sitter'
4+
5+
import { untildify } from './fs'
6+
7+
// Until the grammar supports sourcing, we use this little regular expression
8+
const SOURCED_FILES_REG_EXP = /^(?:\t|[ ])*(?:source|[.])\s*(\S*)/gm
9+
10+
export function getSourcedUris({
11+
fileContent,
12+
fileUri,
13+
}: {
14+
fileContent: string
15+
fileUri: string
16+
}): Set<string> {
17+
const uris: Set<string> = new Set([])
18+
let match: RegExpExecArray | null
19+
20+
while ((match = SOURCED_FILES_REG_EXP.exec(fileContent)) !== null) {
21+
const relativePath = match[1]
22+
const sourcedUri = getSourcedUri({ relativePath, uri: fileUri })
23+
if (sourcedUri) {
24+
uris.add(sourcedUri)
25+
}
26+
}
27+
28+
return uris
29+
}
30+
31+
/**
32+
* Investigates if the given position is a path to a sourced file and maps it
33+
* to a location. Useful for jump to definition.
34+
* @returns an optional location
35+
*/
36+
export function getSourcedLocation({
37+
tree,
38+
position,
39+
uri,
40+
word,
41+
}: {
42+
tree: Parser.Tree
43+
position: { line: number; character: number }
44+
uri: string
45+
word: string
46+
}): LSP.Location | null {
47+
// NOTE: when a word is a file path to a sourced file, we return a location to
48+
// that file.
49+
if (tree.rootNode) {
50+
const node = tree.rootNode.descendantForPosition({
51+
row: position.line,
52+
column: position.character,
53+
})
54+
55+
if (!node || node.text.trim() !== word) {
56+
throw new Error('Implementation error: word was not found at the given position')
57+
}
58+
59+
const isSourced = node.previousNamedSibling
60+
? ['.', 'source'].includes(node.previousNamedSibling.text.trim())
61+
: false
62+
63+
const sourcedUri = isSourced ? getSourcedUri({ relativePath: word, uri }) : null
64+
65+
if (sourcedUri) {
66+
return LSP.Location.create(sourcedUri, LSP.Range.create(0, 0, 0, 0))
67+
}
68+
}
69+
70+
return null
71+
}
72+
73+
const mapPathToUri = (path: string): string => path.replace('file:', 'file://')
74+
75+
const getSourcedUri = ({
76+
relativePath,
77+
uri,
78+
}: {
79+
relativePath: string
80+
uri: string
81+
}): string | null => {
82+
// NOTE: improvements:
83+
// - we could try to resolve the path
84+
// - "If filename does not contain a slash, file names in PATH are used to find
85+
// the directory containing filename." (see https://ss64.com/osx/source.html)
86+
87+
const resultPath = relativePath.startsWith('~')
88+
? untildify(relativePath)
89+
: path.join(path.dirname(uri), relativePath)
90+
91+
return mapPathToUri(resultPath)
92+
}

0 commit comments

Comments
 (0)