@@ -4,49 +4,65 @@ import * as LSP from 'vscode-languageserver'
4
4
import * as Parser from 'web-tree-sitter'
5
5
6
6
import { untildify } from './fs'
7
+ import { logger } from './logger'
8
+ import * as TreeSitterUtil from './tree-sitter'
7
9
8
- // Until the grammar supports sourcing, we use this little regular expression
9
- const SOURCING_STATEMENTS = / ^ (?: \t | [ ] ) * (?: s o u r c e | [ . ] ) \s * ( \S * ) /
10
10
const SOURCING_COMMANDS = [ 'source' , '.' ]
11
11
12
12
/**
13
13
* Analysis the given file content and returns a set of URIs that are
14
14
* sourced. Note that the URIs are resolved.
15
15
*/
16
16
export function getSourcedUris ( {
17
- fileContent,
18
17
fileUri,
19
18
rootPath,
20
19
tree,
21
20
} : {
22
- fileContent : string
23
21
fileUri : string
24
22
rootPath : string | null
25
23
tree : Parser . Tree
26
24
} ) : Set < string > {
27
25
const uris : Set < string > = new Set ( [ ] )
28
26
const rootPaths = [ path . dirname ( fileUri ) , rootPath ] . filter ( Boolean ) as string [ ]
29
27
30
- fileContent . split ( / \r ? \n / ) . forEach ( ( line , lineIndex ) => {
31
- const match = line . match ( SOURCING_STATEMENTS )
32
- if ( match ) {
33
- const [ statement , word ] = match
34
-
35
- if ( tree . rootNode ) {
36
- const node = tree . rootNode . descendantForPosition ( {
37
- row : lineIndex ,
38
- column : statement . length - 2 ,
39
- } )
40
- if ( [ 'heredoc_body' , 'raw_string' ] . includes ( node ?. type ) ) {
41
- return
28
+ // find all source commands in the tree
29
+ TreeSitterUtil . forEach ( tree . rootNode , ( node ) => {
30
+ if ( node . type === 'command' ) {
31
+ const [ commandNameNode , argumentNode ] = node . namedChildren
32
+ if (
33
+ commandNameNode . type === 'command_name' &&
34
+ SOURCING_COMMANDS . includes ( commandNameNode ?. text )
35
+ ) {
36
+ let word = null
37
+ if ( argumentNode . type === 'word' ) {
38
+ word = argumentNode . text
39
+ } else if ( argumentNode . type === 'string' ) {
40
+ if ( argumentNode . namedChildren . length === 0 ) {
41
+ word = argumentNode . text . slice ( 1 , - 1 )
42
+ } else if (
43
+ argumentNode . namedChildren . every ( ( n ) => n . type === 'simple_expansion' )
44
+ ) {
45
+ // not supported
46
+ } else {
47
+ logger . warn (
48
+ 'Sourcing: unhandled argumentNode=string case' ,
49
+ argumentNode . namedChildren . map ( ( c ) => ( { type : c . type , text : c . text } ) ) ,
50
+ )
51
+ }
52
+ } else {
53
+ logger . warn ( 'Sourcing: unhandled argumentNode case' , argumentNode . type )
42
54
}
43
- }
44
55
45
- const sourcedUri = getSourcedUri ( { rootPaths, word } )
46
- if ( sourcedUri ) {
47
- uris . add ( sourcedUri )
56
+ if ( word ) {
57
+ const sourcedUri = getSourcedUri ( { rootPaths, word } )
58
+ if ( sourcedUri ) {
59
+ uris . add ( sourcedUri )
60
+ }
61
+ }
48
62
}
49
63
}
64
+
65
+ return true
50
66
} )
51
67
52
68
return uris
@@ -102,17 +118,6 @@ export function getSourcedLocation({
102
118
return null
103
119
}
104
120
105
- const stripQuotes = ( path : string ) : string => {
106
- const first = path [ 0 ]
107
- const last = path [ path . length - 1 ]
108
-
109
- if ( first === last && [ `"` , `'` ] . includes ( first ) ) {
110
- return path . slice ( 1 , - 1 )
111
- }
112
-
113
- return path
114
- }
115
-
116
121
/**
117
122
* Tries to parse the given path and returns a URI if possible.
118
123
* - Filters out dynamic sources
@@ -131,27 +136,20 @@ function getSourcedUri({
131
136
rootPaths : string [ ]
132
137
word : string
133
138
} ) : string | null {
134
- let unquotedPath = stripQuotes ( word )
135
-
136
- if ( unquotedPath . includes ( '$' ) ) {
137
- // NOTE: we don't support dynamic sourcing
138
- return null
139
- }
140
-
141
- if ( unquotedPath . startsWith ( '~' ) ) {
142
- unquotedPath = untildify ( unquotedPath )
139
+ if ( word . startsWith ( '~' ) ) {
140
+ word = untildify ( word )
143
141
}
144
142
145
- if ( unquotedPath . startsWith ( '/' ) ) {
146
- if ( fs . existsSync ( unquotedPath ) ) {
147
- return `file://${ unquotedPath } `
143
+ if ( word . startsWith ( '/' ) ) {
144
+ if ( fs . existsSync ( word ) ) {
145
+ return `file://${ word } `
148
146
}
149
147
return null
150
148
}
151
149
152
150
// resolve relative path
153
151
for ( const rootPath of rootPaths ) {
154
- const potentialPath = path . join ( rootPath . replace ( 'file://' , '' ) , unquotedPath )
152
+ const potentialPath = path . join ( rootPath . replace ( 'file://' , '' ) , word )
155
153
156
154
// check if path is a file
157
155
if ( fs . existsSync ( potentialPath ) ) {
0 commit comments