Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ BrighterScript adds several new features to the BrightScript language such as na

- Full BrighterScript support for syntax checking, validation, and intellisense is available within the [Brightscript Language](https://marketplace.visualstudio.com/items?itemName=celsoaf.brightscript) VSCode extension.

- Configure the language server behavior using [language server settings](https://github.com/rokucommunity/brighterscript/blob/master/docs/language-server.md).

- And if it's not enough, the [plugin API](https://github.com/rokucommunity/brighterscript/blob/master/docs/plugins.md) allows extending the compiler to provide extra diagnostics or transformations.

## Who uses Brighterscript?
Expand Down
81 changes: 81 additions & 0 deletions docs/language-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Language Server Settings

The BrighterScript language server provides several configuration options to customize its behavior for your project.

## Configuration

All language server settings are configured under the `brightscript.languageServer` section in your VS Code settings.

### projectDiscoveryExclude

The `projectDiscoveryExclude` setting allows you to exclude files and directories from project discovery using glob patterns. This is useful when you want to prevent the language server from creating projects for certain directories or files.

**Type:** `Record<string, boolean>`

**Default:** `undefined`

**Example:**
```json
{
"brightscript.languageServer.projectDiscoveryExclude": {
"**/test/**": true,
"**/node_modules/**": true,
"**/.build/**": true,
"**/dist/**": true
}
}
```

This setting works similarly to VS Code's `files.exclude` setting but is specifically for controlling which files are considered during project discovery.

### files.watcherExclude Support

The language server now automatically respects VS Code's built-in `files.watcherExclude` setting. This setting allows you to exclude files from being watched for changes, which can improve performance in large projects.

**Type:** `Record<string, boolean>`

**Example:**
```json
{
"files.watcherExclude": {
"**/tmp/**": true,
"**/cache/**": true,
"**/node_modules/**": true,
"**/.git/objects/**": true
}
}
```

Files matching patterns in `files.watcherExclude` will be ignored during file watching, which can help reduce system resource usage and improve performance.

## How Exclusion Works

The language server uses multiple exclusion sources to determine which files to process:

1. **VS Code's built-in exclusions:**
- `files.exclude` - Files excluded from VS Code's file explorer
- `files.watcherExclude` - Files excluded from file watching
- `search.exclude` - Files excluded from search operations

2. **BrighterScript-specific exclusions:**
- `brightscript.languageServer.projectDiscoveryExclude` - Files excluded from project discovery

3. **Standard exclusions:**
- `.gitignore` patterns
- Common directories like `node_modules`, `.git`, `out`, `.roku-deploy-staging`

All these exclusion patterns work together to provide comprehensive control over which files the language server processes.

## Performance Tips

For better performance in large projects, consider excluding:

- Build output directories (`**/dist/**`, `**/build/**`, `**/out/**`)
- Test files that don't need language server analysis (`**/test/**`, `**/*.spec.*`)
- Temporary directories (`**/tmp/**`, `**/cache/**`)
- Package dependencies (`**/node_modules/**`)
- Version control directories (`**/.git/**`)

## Compatibility

Both `projectDiscoveryExclude` and `files.watcherExclude` support work with existing exclusion mechanisms and maintain backward compatibility with existing configurations.
169 changes: 167 additions & 2 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,14 +544,179 @@ describe('LanguageServer', () => {
languageServer: {
enableThreading: false,
enableProjectDiscovery: true,
logLevel: 'info',
projectDiscoveryMaxDepth: 15
projectDiscoveryMaxDepth: 15,
projectDiscoveryExclude: undefined,
logLevel: 'info'
}
}
]);
});
});

describe('projectDiscoveryExclude and files.watcherExclude', () => {
it('includes projectDiscoveryExclude in workspace configuration', async () => {
const projectDiscoveryExclude = {
'**/test/**': true,
'node_modules/**': true
};

sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'brightscript') {
return Promise.resolve({
languageServer: {
projectDiscoveryExclude: projectDiscoveryExclude
}
});
}
return Promise.resolve({});
});

server.run();
const configs = await server['getWorkspaceConfigs']();
expect(configs[0].languageServer.projectDiscoveryExclude).to.deep.equal(projectDiscoveryExclude);
});

it('includes files.watcherExclude in workspace exclude patterns', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'files') {
return Promise.resolve({
exclude: { 'node_modules': true },
watcherExclude: {
'**/tmp/**': true,
'**/cache/**': true
}
});
}
return Promise.resolve({});
});

server.run();
const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.include('**/tmp/**');
expect(excludeGlobs).to.include('**/cache/**');
});

it('includes projectDiscoveryExclude in workspace exclude patterns', async () => {
const projectDiscoveryExclude = {
'**/test/**': true,
'**/node_modules/**': true,
'**/.build/**': true
};

sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'brightscript') {
return Promise.resolve({
languageServer: {
projectDiscoveryExclude: projectDiscoveryExclude
}
});
}
return Promise.resolve({});
});

server.run();
const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.include('**/test/**');
expect(excludeGlobs).to.include('**/node_modules/**');
expect(excludeGlobs).to.include('**/.build/**');
});

it('handles undefined projectDiscoveryExclude without crashing', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'brightscript') {
return Promise.resolve({
languageServer: {
// projectDiscoveryExclude is undefined
}
});
}
return Promise.resolve({});
});

server.run();
const configs = await server['getWorkspaceConfigs']();
expect(configs[0].languageServer.projectDiscoveryExclude).to.be.undefined;

// Should not crash during pathFilterer rebuild
await server['rebuildPathFilterer']();
});

it('handles undefined files.watcherExclude without crashing', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'files') {
return Promise.resolve({
exclude: { '**/node_modules/**/*': true }
// watcherExclude is undefined
});
}
return Promise.resolve({});
});

server.run();
const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.eql([
'**/node_modules/**/*'
]);
});

it('handles null/undefined configuration sections without crashing', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
return Promise.resolve(null);
});

server.run();
const configs = await server['getWorkspaceConfigs']();
expect(configs[0].languageServer.projectDiscoveryExclude).to.be.undefined;

const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.eql([]);
});

it('handles empty objects for configuration sections without crashing', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
return Promise.resolve({});
});

server.run();
const configs = await server['getWorkspaceConfigs']();
expect(configs[0].languageServer.projectDiscoveryExclude).to.be.undefined;

const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.eql([]);
});

it('handles mixed defined/undefined settings without crashing', async () => {
sinon.stub(server as any, 'getClientConfiguration').callsFake((workspaceFolder, section) => {
if (section === 'brightscript') {
return Promise.resolve({
languageServer: {
projectDiscoveryExclude: {
'**/test/**/*': true
}
}
});
} else if (section === 'files') {
return Promise.resolve({
exclude: { '**/excludeMe/**/*': true }
// watcherExclude is undefined
});
}
return Promise.resolve({});
});

server.run();

const excludeGlobs = await server['getWorkspaceExcludeGlobs'](workspaceFolders[0]);
expect(excludeGlobs).to.eql([
'**/excludeMe/**/*',
'**/test/**/*'
]);

// Should not crash during pathFilterer rebuild
await server['rebuildPathFilterer']();
});
});

describe('onInitialize', () => {
it('sets capabilities', async () => {
server['hasConfigurationCapability'] = false;
Expand Down
54 changes: 33 additions & 21 deletions src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,12 +430,14 @@ export class LanguageServer {
return {
workspaceFolder: workspaceFolder,
excludePatterns: await this.getWorkspaceExcludeGlobs(workspaceFolder),
projects: this.normalizeProjectPaths(workspaceFolder, brightscriptConfig.projects),
projects: this.normalizeProjectPaths(workspaceFolder, brightscriptConfig?.projects),
languageServer: {
enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
enableProjectDiscovery: brightscriptConfig.languageServer?.enableProjectDiscovery ?? LanguageServer.enableProjectDiscoveryDefault,
logLevel: brightscriptConfig?.languageServer?.logLevel,
projectDiscoveryMaxDepth: brightscriptConfig?.languageServer?.projectDiscoveryMaxDepth ?? 15
enableThreading: brightscriptConfig?.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault,
enableProjectDiscovery: brightscriptConfig?.languageServer?.enableProjectDiscovery ?? LanguageServer.enableProjectDiscoveryDefault,
projectDiscoveryMaxDepth: brightscriptConfig?.languageServer?.projectDiscoveryMaxDepth ?? 15,
projectDiscoveryExclude: brightscriptConfig?.languageServer?.projectDiscoveryExclude,
logLevel: brightscriptConfig?.languageServer?.logLevel

}
};
})
Expand Down Expand Up @@ -673,33 +675,42 @@ export class LanguageServer {
}

/**
* Ask the client for the list of `files.exclude` patterns. Useful when determining if we should process a file
* Ask the client for the list of `files.exclude` and `files.watcherExclude` patterns. Useful when determining if we should process a file
*/
private async getWorkspaceExcludeGlobs(workspaceFolder: string): Promise<string[]> {
const filesConfig = await this.getClientConfiguration<{ exclude: string[] }>(workspaceFolder, 'files');
const fileExcludes = this.extractExcludes(filesConfig);

const searchConfig = await this.getClientConfiguration<{ exclude: string[] }>(workspaceFolder, 'search');
const searchExcludes = this.extractExcludes(searchConfig);
const filesConfig = await this.getClientConfiguration<{ exclude: Record<string, boolean>; watcherExclude: Record<string, boolean> }>(workspaceFolder, 'files');
const searchConfig = await this.getClientConfiguration<{ exclude: Record<string, boolean> }>(workspaceFolder, 'search');
const languageServerConfig = await this.getClientConfiguration<BrightScriptClientConfiguration>(workspaceFolder, 'brightscript');

return [...fileExcludes, ...searchExcludes];
return [
...this.extractExcludes(filesConfig?.exclude),
...this.extractExcludes(filesConfig?.watcherExclude),
...this.extractExcludes(searchConfig?.exclude),
...this.extractExcludes(languageServerConfig?.languageServer?.projectDiscoveryExclude)
];
}

private extractExcludes(config: { exclude: string[] }): string[] {
if (!config?.exclude) {
private extractExcludes(exclude: Record<string, boolean>): string[] {
//if the exclude is not defined, return an empty array
if (!exclude) {
return [];
}
return Object
.keys(config.exclude)
.filter(x => config.exclude[x])
.keys(exclude)
.filter(x => exclude[x])
//vscode files.exclude patterns support ignoring folders without needing to add `**/*`. So for our purposes, we need to
//append **/* to everything without a file extension or magic at the end
.map(pattern => [
//send the pattern as-is (this handles weird cases and exact file matches)
pattern,
.map(pattern => {
const result = [
//send the pattern as-is (this handles weird cases and exact file matches)
pattern
];
//treat the pattern as a directory (no harm in doing this because if it's a file, the pattern will just never match anything)
`${pattern}/**/*`
])
if (!pattern.endsWith('/**/*')) {
result.push(`${pattern}/**/*`);
}
return result;
})
.flat(1);
}

Expand Down Expand Up @@ -825,6 +836,7 @@ export interface BrightScriptClientConfiguration {
languageServer: {
enableThreading: boolean;
enableProjectDiscovery: boolean;
projectDiscoveryExclude?: Record<string, boolean>;
logLevel: LogLevel | string;
projectDiscoveryMaxDepth?: number;
};
Expand Down
4 changes: 4 additions & 0 deletions src/lsp/ProjectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@ export interface WorkspaceConfig {
* Should the language server automatically discover projects in this workspace?
*/
enableProjectDiscovery: boolean;
/**
* A list of glob patterns used to _exclude_ files from project discovery
*/
projectDiscoveryExclude?: Record<string, boolean>;
/**
* The log level to use for this workspace
*/
Expand Down
Loading