Skip to content

Commit bba68bb

Browse files
authored
Merge pull request #187 from mads-hartmann/improve-globbing
Extend and make glob pattern configurable
2 parents 7cfec68 + e06d158 commit bba68bb

File tree

16 files changed

+183
-47
lines changed

16 files changed

+183
-47
lines changed

server/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Bash Language Server
22

3+
## 1.8.0
4+
5+
* Extend file glob used for pre-analyzing files from `**/*.sh` to `**/*@(.sh|.inc|.bash|.command)`
6+
* Make file glob configurable with `GLOB_PATTERN` environment variable
7+
38
## 1.7.0
49

510
* Add PATH tilde expansion

server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A language server for Bash",
44
"author": "Mads Hartmann",
55
"license": "MIT",
6-
"version": "1.7.0",
6+
"version": "1.8.0",
77
"publisher": "mads-hartmann",
88
"main": "./out/server.js",
99
"typings": "./out/server.d.ts",
@@ -18,7 +18,7 @@
1818
"node": "*"
1919
},
2020
"dependencies": {
21-
"glob": "^7.1.2",
21+
"glob": "^7.1.6",
2222
"request": "^2.83.0",
2323
"request-promise-native": "^1.0.5",
2424
"turndown": "^4.0.2",

server/src/__tests__/analyzer.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import FIXTURES from '../../../testing/fixtures'
1+
import FIXTURES, { FIXTURE_FOLDER } from '../../../testing/fixtures'
22
import Analyzer from '../analyser'
33
import { initializeParser } from '../parser'
44

@@ -97,3 +97,39 @@ describe('findSymbolCompletions', () => {
9797
expect(analyzer.findSymbolCompletions(CURRENT_URI)).toMatchSnapshot()
9898
})
9999
})
100+
101+
describe('fromRoot', () => {
102+
it('initializes an analyzer from a root', async () => {
103+
const parser = await initializeParser()
104+
105+
jest.spyOn(Date, 'now').mockImplementation(() => 0)
106+
107+
const connection: any = {
108+
console: {
109+
log: jest.fn(),
110+
},
111+
}
112+
113+
const newAnalyzer = await Analyzer.fromRoot({
114+
connection,
115+
rootPath: FIXTURE_FOLDER,
116+
parser,
117+
})
118+
119+
expect(newAnalyzer).toBeDefined()
120+
121+
const FIXTURE_FILES_MATCHING_GLOB = 8
122+
const LOG_LINES = FIXTURE_FILES_MATCHING_GLOB + 3
123+
124+
expect(connection.console.log).toHaveBeenCalledTimes(LOG_LINES)
125+
expect(connection.console.log).toHaveBeenNthCalledWith(
126+
1,
127+
expect.stringContaining('Analyzing files matching'),
128+
)
129+
130+
expect(connection.console.log).toHaveBeenNthCalledWith(
131+
LOG_LINES,
132+
'Analyzer finished after 0 seconds',
133+
)
134+
})
135+
})

server/src/__tests__/config.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ describe('getExplainshellEndpoint', () => {
77
expect(result).toBeNull()
88
})
99

10+
it('default to null in case of an empty string', () => {
11+
process.env = {
12+
EXPLAINSHELL_ENDPOINT: '',
13+
}
14+
const result = config.getExplainshellEndpoint()
15+
expect(result).toBeNull()
16+
})
17+
1018
it('parses environment variable', () => {
1119
process.env = {
1220
EXPLAINSHELL_ENDPOINT: 'localhost:8080',
@@ -16,6 +24,30 @@ describe('getExplainshellEndpoint', () => {
1624
})
1725
})
1826

27+
describe('getGlobPattern', () => {
28+
it('default to a basic glob', () => {
29+
process.env = {}
30+
const result = config.getGlobPattern()
31+
expect(result).toEqual(config.DEFAULT_GLOB_PATTERN)
32+
})
33+
34+
it('default to a basic glob in case of an empty string', () => {
35+
process.env = {
36+
GLOB_PATTERN: '',
37+
}
38+
const result = config.getGlobPattern()
39+
expect(result).toEqual(config.DEFAULT_GLOB_PATTERN)
40+
})
41+
42+
it('parses environment variable', () => {
43+
process.env = {
44+
GLOB_PATTERN: '*.*',
45+
}
46+
const result = config.getGlobPattern()
47+
expect(result).toEqual('*.*')
48+
})
49+
})
50+
1951
describe('highlightParsingError', () => {
2052
it('default to true', () => {
2153
process.env = {}

server/src/analyser.ts

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as fs from 'fs'
2-
import * as glob from 'glob'
3-
import * as Path from 'path'
42
import * as request from 'request-promise-native'
53
import * as URI from 'urijs'
64
import * as LSP from 'vscode-languageserver'
75
import * as Parser from 'web-tree-sitter'
86

7+
import { getGlobPattern } from './config'
98
import { uniqueBasedOnHash } from './util/array'
109
import { flattenArray, flattenObjectValues } from './util/flatten'
10+
import { getFilePaths } from './util/fs'
1111
import * as TreeSitterUtil from './util/tree-sitter'
1212

1313
type Kinds = { [type: string]: LSP.SymbolKind }
@@ -27,10 +27,10 @@ export default class Analyzer {
2727
* Initialize the Analyzer based on a connection to the client and an optional
2828
* root path.
2929
*
30-
* If the rootPath is provided it will initialize all *.sh files it can find
31-
* anywhere on that path.
30+
* If the rootPath is provided it will initialize all shell files it can find
31+
* anywhere on that path. This non-exhaustive glob is used to preload the parser.
3232
*/
33-
public static fromRoot({
33+
public static async fromRoot({
3434
connection,
3535
rootPath,
3636
parser,
@@ -39,39 +39,37 @@ export default class Analyzer {
3939
rootPath: string | null
4040
parser: Parser
4141
}): Promise<Analyzer> {
42-
// This happens if the users opens a single bash script without having the
43-
// 'window' associated with a specific project.
44-
if (!rootPath) {
45-
return Promise.resolve(new Analyzer(parser))
46-
}
42+
const analyzer = new Analyzer(parser)
43+
44+
if (rootPath) {
45+
const globPattern = getGlobPattern()
46+
connection.console.log(
47+
`Analyzing files matching glob "${globPattern}" inside ${rootPath}`,
48+
)
4749

48-
return new Promise((resolve, reject) => {
49-
glob('**/*.sh', { cwd: rootPath }, (err, paths) => {
50-
if (err != null) {
51-
reject(err)
52-
} else {
53-
const analyzer = new Analyzer(parser)
54-
paths.forEach(p => {
55-
const absolute = Path.join(rootPath, p)
56-
// only analyze files, glob pattern may match directories
57-
if (fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) {
58-
const uri = `file://${absolute}`
59-
connection.console.log(`Analyzing ${uri}`)
60-
analyzer.analyze(
61-
uri,
62-
LSP.TextDocument.create(
63-
uri,
64-
'shell',
65-
1,
66-
fs.readFileSync(absolute, 'utf8'),
67-
),
68-
)
69-
}
70-
})
71-
resolve(analyzer)
72-
}
50+
const lookupStartTime = Date.now()
51+
const getTimePassed = (): string =>
52+
`${(Date.now() - lookupStartTime) / 1000} seconds`
53+
54+
// NOTE: An alternative would be to preload all files and analyze their
55+
// shebang or mimetype, but it would be fairly expensive.
56+
const filePaths = await getFilePaths({ globPattern, rootPath })
57+
58+
connection.console.log(
59+
`Glob resolved with ${filePaths.length} files after ${getTimePassed()}`,
60+
)
61+
62+
filePaths.forEach(filePath => {
63+
const uri = `file://${filePath}`
64+
connection.console.log(`Analyzing ${uri}`)
65+
const fileContent = fs.readFileSync(filePath, 'utf8')
66+
analyzer.analyze(uri, LSP.TextDocument.create(uri, 'shell', 1, fileContent))
7367
})
74-
})
68+
69+
connection.console.log(`Analyzer finished after ${getTimePassed()}`)
70+
}
71+
72+
return analyzer
7573
}
7674

7775
private parser: Parser

server/src/config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
export const DEFAULT_GLOB_PATTERN = '**/*@(.sh|.inc|.bash|.command)'
2+
13
export function getExplainshellEndpoint(): string | null {
24
const { EXPLAINSHELL_ENDPOINT } = process.env
3-
return typeof EXPLAINSHELL_ENDPOINT !== 'undefined' ? EXPLAINSHELL_ENDPOINT : null
5+
return typeof EXPLAINSHELL_ENDPOINT === 'string' && EXPLAINSHELL_ENDPOINT.trim() !== ''
6+
? EXPLAINSHELL_ENDPOINT
7+
: null
8+
}
9+
10+
export function getGlobPattern(): string {
11+
const { GLOB_PATTERN } = process.env
12+
return typeof GLOB_PATTERN === 'string' && GLOB_PATTERN.trim() !== ''
13+
? GLOB_PATTERN
14+
: DEFAULT_GLOB_PATTERN
415
}
516

617
export function getHighlightParsingError(): boolean {

server/src/util/fs.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Fs from 'fs'
2+
import * as glob from 'glob'
23
import * as Os from 'os'
34

45
export function getStats(path: string): Promise<Fs.Stats> {
@@ -20,3 +21,24 @@ export function untildify(pathWithTilde: string): string {
2021
? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
2122
: pathWithTilde
2223
}
24+
25+
export async function getFilePaths({
26+
globPattern,
27+
rootPath,
28+
}: {
29+
globPattern: string
30+
rootPath: string
31+
}): Promise<string[]> {
32+
return new Promise((resolve, reject) => {
33+
glob(globPattern, { cwd: rootPath, nodir: true, absolute: true }, function(
34+
err,
35+
files,
36+
) {
37+
if (err) {
38+
return reject(err)
39+
}
40+
41+
resolve(files)
42+
})
43+
})
44+
}

server/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,10 @@ getpass@^0.1.1:
328328
dependencies:
329329
assert-plus "^1.0.0"
330330

331-
glob@^7.1.2:
332-
version "7.1.2"
333-
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
334-
integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
331+
glob@^7.1.6:
332+
version "7.1.6"
333+
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
334+
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
335335
dependencies:
336336
fs.realpath "^1.0.0"
337337
inflight "^1.0.4"

testing/fixtures.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import * as fs from 'fs'
22
import * as path from 'path'
33
import * as LSP from 'vscode-languageserver'
44

5-
const base = path.join(__dirname, './fixtures/')
5+
export const FIXTURE_FOLDER = path.join(__dirname, './fixtures/')
66

77
function getFixture(filename: string) {
88
return LSP.TextDocument.create(
99
'foo',
1010
'bar',
1111
0,
12-
fs.readFileSync(path.join(base, filename), 'utf8'),
12+
fs.readFileSync(path.join(FIXTURE_FOLDER, filename), 'utf8'),
1313
)
1414
}
1515

testing/fixtures/extension

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "It works, but is not parsed initially"

0 commit comments

Comments
 (0)