Skip to content

Commit 4e7ab70

Browse files
committed
Add utilities for supporting sourcing
1 parent 2c068eb commit 4e7ab70

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 a set of sourced files', () => {
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+
source "./my_quoted_file.sh"
31+
32+
source "$LIBPATH" # dynamic imports not supported
33+
34+
# conditional is currently not supported
35+
if [[ -z $__COMPLETION_LIB_LOADED ]]; then source "$LIBPATH" ; fi
36+
`,
37+
fileUri,
38+
})
39+
expect(result).toEqual(
40+
new Set([
41+
`${fileDirectory}/file-in-path.sh`, // as we don't resolve it, we hope it is here
42+
`${fileDirectory}/bin/f.inc`,
43+
`${fileDirectory}/x`,
44+
`${fileDirectory}/relative/to-this.sh`,
45+
`${homedir()}/myscript`,
46+
`${fileDirectory}/my_quoted_file.sh`,
47+
]),
48+
)
49+
})
50+
})

server/src/util/sourcing.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 stripQuotes = (path: string): string => {
76+
const first = path[0]
77+
const last = path[path.length - 1]
78+
79+
if (first === last && [`"`, `'`].includes(first)) {
80+
return path.slice(1, -1)
81+
}
82+
83+
return path
84+
}
85+
86+
const getSourcedUri = ({
87+
relativePath,
88+
uri,
89+
}: {
90+
relativePath: string
91+
uri: string
92+
}): string | null => {
93+
// NOTE: improvements:
94+
// - we could try to resolve the path
95+
// - "If filename does not contain a slash, file names in PATH are used to find
96+
// the directory containing filename." (see https://ss64.com/osx/source.html)
97+
const unquotedRelativePath = stripQuotes(relativePath)
98+
99+
if (unquotedRelativePath.includes('$')) {
100+
return null
101+
}
102+
103+
const resultPath = unquotedRelativePath.startsWith('~')
104+
? untildify(unquotedRelativePath)
105+
: path.join(path.dirname(uri), unquotedRelativePath)
106+
107+
return mapPathToUri(resultPath)
108+
}

0 commit comments

Comments
 (0)