Skip to content
Draft
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
3 changes: 2 additions & 1 deletion src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,8 @@ describe('LanguageServer', () => {
projectDiscoveryExclude: undefined,
logLevel: 'info',
projectActivationConcurrencyLimit: undefined,
maxWorkerThreads: undefined
maxWorkerThreads: undefined,
lazyProjectActivation: false
}
}
]);
Expand Down
20 changes: 19 additions & 1 deletion src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export class LanguageServer {
* The default project discovery setting for the language server. Can be overridden by per-workspace settings
*/
public static enableProjectDiscoveryDefault = true;
/**
* The default lazy project activation setting for the language server. Can be overridden by per-workspace settings
*/
public static lazyProjectActivationDefault = false;

/**
* The default number of projects that are permitted to activate concurrently.
Expand Down Expand Up @@ -206,6 +210,9 @@ export class LanguageServer {
//
this.documents.onDidChangeContent(this.onTextDocumentDidChangeContent.bind(this));

//whenever a document gets opened (fires before the first onDidChangeContent for that document)
this.documents.onDidOpen(this.onDocumentOpen.bind(this));

//whenever a document gets closed
this.documents.onDidClose(this.onDocumentClose.bind(this));

Expand Down Expand Up @@ -555,6 +562,15 @@ export class LanguageServer {
await this.projectManager.handleFileChanges(changes);
}

@AddStackToErrorMessage
private async onDocumentOpen(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
this.logger.debug('onDocumentOpen', event.document.uri);

await this.projectManager.handleFileOpen({
srcPath: util.uriToPath(event.document.uri)
});
}

@AddStackToErrorMessage
private async onDocumentClose(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
this.logger.debug('onDocumentClose', event.document.uri);
Expand Down Expand Up @@ -611,7 +627,8 @@ export class LanguageServer {
projectDiscoveryExclude: brightscriptConfig?.languageServer?.projectDiscoveryExclude,
logLevel: brightscriptConfig?.languageServer?.logLevel,
projectActivationConcurrencyLimit: brightscriptConfig?.languageServer?.projectActivationConcurrencyLimit,
maxWorkerThreads: brightscriptConfig?.languageServer?.maxWorkerThreads
maxWorkerThreads: brightscriptConfig?.languageServer?.maxWorkerThreads,
lazyProjectActivation: brightscriptConfig?.languageServer?.lazyProjectActivation ?? LanguageServer.lazyProjectActivationDefault
}
};
})
Expand Down Expand Up @@ -1083,6 +1100,7 @@ export interface BrightScriptClientConfiguration {
projectDiscoveryMaxDepth?: number;
projectActivationConcurrencyLimit?: number;
maxWorkerThreads?: number;
lazyProjectActivation?: boolean;
};
}

Expand Down
125 changes: 125 additions & 0 deletions src/lsp/ProjectManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,131 @@ describe('ProjectManager', () => {
});
});

describe('lazy project activation', () => {
const lazyWorkspaceSettings = {
...workspaceSettings,
languageServer: {
...workspaceSettings.languageServer,
lazyProjectActivation: true
}
};

beforeEach(() => {
fsExtra.outputFileSync(`${rootDir}/project1/bsconfig.json`, '');
fsExtra.outputFileSync(`${rootDir}/project1/source/main.brs`, `sub main()\nend sub`);
fsExtra.outputFileSync(`${rootDir}/project2/bsconfig.json`, '');
fsExtra.outputFileSync(`${rootDir}/project2/source/main.brs`, `sub main()\nend sub`);
});

async function openFile(srcPath: string, contents: string) {
await manager.handleFileOpen({ srcPath: srcPath });
await manager.handleFileChanges([{
srcPath: srcPath,
type: FileChangeType.Changed,
fileContents: contents,
allowStandaloneProject: true
}]);
}

it('activates every project when the setting is disabled', async () => {
await manager.syncProjects([workspaceSettings]);
expect(manager.projects.map(x => x.projectKey).sort()).to.eql([
s`${rootDir}/project1/bsconfig.json`,
s`${rootDir}/project2/bsconfig.json`
]);
expect(manager['dormantProjects'].size).to.eql(0);
});

it('leaves projects dormant when none of their files are open', async () => {
await manager.syncProjects([lazyWorkspaceSettings]);
expect(manager.projects).to.be.empty;
expect([...manager['dormantProjects'].keys()].sort()).to.eql([
s`${rootDir}/project1/bsconfig.json`,
s`${rootDir}/project2/bsconfig.json`
]);
});

it('activates the only project even when the setting is enabled', async () => {
fsExtra.removeSync(`${rootDir}/project2`);
await manager.syncProjects([lazyWorkspaceSettings]);
expect(manager.projects.map(x => x.projectKey)).to.eql([
s`${rootDir}/project1/bsconfig.json`
]);
expect(manager['dormantProjects'].size).to.eql(0);
});

it('activates a dormant project when one of its files is opened', async () => {
await manager.syncProjects([lazyWorkspaceSettings]);

await openFile(s`${rootDir}/project1/source/main.brs`, `sub main()\n doesNotExist()\nend sub`);
await manager.onIdle();

expect(manager.projects.map(x => x.projectKey)).to.eql([
s`${rootDir}/project1/bsconfig.json`
]);
expect([...manager['dormantProjects'].keys()]).to.eql([
s`${rootDir}/project2/bsconfig.json`
]);
//the file was handled by the woken project, so no standalone project should exist
expect(manager['standaloneProjects'].size).to.eql(0);

//the woken project was validated using the in-memory contents of the opened file
const diagnostics = await onNextDiagnostics();
expectDiagnostics(diagnostics, [
DiagnosticMessages.cannotFindFunction('doesNotExist').message
]);
});

it('activates projects that have an open file during sync', async () => {
await manager.handleFileOpen({ srcPath: s`${rootDir}/project2/source/main.brs` });
await manager.syncProjects([lazyWorkspaceSettings]);
expect(manager.projects.map(x => x.projectKey)).to.eql([
s`${rootDir}/project2/bsconfig.json`
]);
expect([...manager['dormantProjects'].keys()]).to.eql([
s`${rootDir}/project1/bsconfig.json`
]);
});

it('creates a standalone project for an open file that no dormant project owns', async () => {
await manager.syncProjects([lazyWorkspaceSettings]);
await openFile(s`${rootDir}/loose.brs`, `sub loose()\nend sub`);
await manager.onIdle();
expect(manager['dormantProjects'].size).to.eql(2);
expect(manager['standaloneProjects'].size).to.eql(1);
});

it('activates dormant projects when the setting gets disabled', async () => {
await manager.syncProjects([lazyWorkspaceSettings]);
expect(manager.projects).to.be.empty;

await manager.syncProjects([workspaceSettings]);
expect(manager.projects.map(x => x.projectKey).sort()).to.eql([
s`${rootDir}/project1/bsconfig.json`,
s`${rootDir}/project2/bsconfig.json`
]);
expect(manager['dormantProjects'].size).to.eql(0);
});

it('uses the files array from bsconfig.json to decide which project owns an opened file', async () => {
fsExtra.outputFileSync(`${rootDir}/project2/bsconfig.json`, JSON.stringify({
rootDir: 'app',
files: ['source/**/*']
}));
fsExtra.outputFileSync(`${rootDir}/project2/app/source/main.brs`, `sub main()\nend sub`);
await manager.syncProjects([lazyWorkspaceSettings]);

//this file is outside of project2's rootDir, so it should not wake project2
await openFile(s`${rootDir}/project2/source/main.brs`, `sub main()\nend sub`);
await manager.onIdle();
expect(manager['dormantProjects'].has(s`${rootDir}/project2/bsconfig.json`)).to.be.true;

await openFile(s`${rootDir}/project2/app/source/main.brs`, `sub main()\nend sub`);
await manager.onIdle();
expect(manager.projects.map(x => x.projectKey)).to.include(s`${rootDir}/project2/bsconfig.json`);
});
});

it('completes promise when project is disposed in the middle of a flow', async function () {
//this test enables real threading without a warm-up hook, so it pays the full worker-thread
//cold-boot cost itself. That boot has been measured at ~18s on macOS CI runners, so give it
Expand Down
Loading
Loading