diff --git a/src/LanguageServer.spec.ts b/src/LanguageServer.spec.ts index 73a826c31..a8efda09d 100644 --- a/src/LanguageServer.spec.ts +++ b/src/LanguageServer.spec.ts @@ -17,13 +17,14 @@ import { createVisitor, WalkMode } from './astUtils/visitors'; import { tempDir, rootDir } from './testHelpers.spec'; import { URI } from 'vscode-uri'; import { BusyStatusTracker } from './BusyStatusTracker'; -import type { BscFile, WorkspaceConfigWithExtras } from '.'; +import type { BscFile } from './interfaces'; import type { Project } from './lsp/Project'; import { LogLevel, Logger, createLogger } from './logging'; import { DiagnosticMessages } from './DiagnosticMessages'; import { standardizePath } from 'roku-deploy'; import undent from 'undent'; import { ProjectManager } from './lsp/ProjectManager'; +import type { WorkspaceConfig } from './lsp/ProjectManager'; const sinon = createSandbox(); @@ -167,7 +168,7 @@ describe('LanguageServer', () => { }); describe('onDidChangeConfiguration', () => { - async function doTest(startingConfigs: WorkspaceConfigWithExtras[], endingConfigs: WorkspaceConfigWithExtras[]) { + async function doTest(startingConfigs: WorkspaceConfig[], endingConfigs: WorkspaceConfig[]) { (server as any)['connection'] = connection; server['workspaceConfigsCache'] = new Map(startingConfigs.map(x => [x.workspaceFolder, x])); @@ -188,7 +189,8 @@ describe('LanguageServer', () => { await doTest([{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -196,7 +198,8 @@ describe('LanguageServer', () => { }], [{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -210,7 +213,8 @@ describe('LanguageServer', () => { await doTest([], [{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -224,7 +228,8 @@ describe('LanguageServer', () => { await doTest([{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -232,7 +237,8 @@ describe('LanguageServer', () => { }, { bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: s`${tempDir}/project2`, @@ -240,7 +246,8 @@ describe('LanguageServer', () => { }], [{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -254,7 +261,8 @@ describe('LanguageServer', () => { await doTest([{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'trace' }, workspaceFolder: workspacePath, @@ -262,7 +270,8 @@ describe('LanguageServer', () => { }], [{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, @@ -649,18 +658,19 @@ describe('LanguageServer', () => { }); describe('rebuildPathFilterer', () => { - let workspaceConfigs: WorkspaceConfigWithExtras[] = []; + let workspaceConfigs: WorkspaceConfig[] = []; beforeEach(() => { workspaceConfigs = [ { bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: workspacePath, excludePatterns: [] - } as WorkspaceConfigWithExtras + } ]; server['connection'] = connection as any; sinon.stub(server as any, 'getWorkspaceConfigs').callsFake(() => Promise.resolve(workspaceConfigs)); @@ -736,7 +746,8 @@ describe('LanguageServer', () => { workspaceConfigs = [{ bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: s`${tempDir}/flavor1`, @@ -744,12 +755,13 @@ describe('LanguageServer', () => { }, { bsconfigPath: undefined, languageServer: { - enableThreading: true, + enableThreading: false, + enableDiscovery: true, logLevel: 'info' }, workspaceFolder: s`${tempDir}/flavor2`, excludePatterns: [] - }] as WorkspaceConfigWithExtras[]; + }]; fsExtra.outputFileSync(s`${workspaceConfigs[0].workspaceFolder}/.gitignore`, undent` dist/ `); diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts index 9ccb6172b..f6041a180 100644 --- a/src/LanguageServer.ts +++ b/src/LanguageServer.ts @@ -61,6 +61,10 @@ export class LanguageServer { * The default threading setting for the language server. Can be overridden by per-workspace settings */ public static enableThreadingDefault = true; + /** + * The default project discovery setting for the language server. Can be overridden by per-workspace settings + */ + public static enableDiscoveryDefault = true; /** * The language server protocol connection, used to send and receive all requests and responses */ @@ -417,7 +421,7 @@ export class LanguageServer { * Get a list of workspaces, and their configurations. * Get only the settings for the workspace that are relevant to the language server. We do this so we can cache this object for use in change detection in the future. */ - private async getWorkspaceConfigs(): Promise { + private async getWorkspaceConfigs(): Promise { //get all workspace folders (we'll use these to get settings) let workspaces = await Promise.all( (await this.connection.workspace.getWorkspaceFolders() ?? []).map(async (x) => { @@ -429,16 +433,17 @@ export class LanguageServer { bsconfigPath: brightscriptConfig.configFile, languageServer: { enableThreading: brightscriptConfig.languageServer?.enableThreading ?? LanguageServer.enableThreadingDefault, + enableDiscovery: brightscriptConfig.languageServer?.enableDiscovery ?? LanguageServer.enableDiscoveryDefault, logLevel: brightscriptConfig?.languageServer?.logLevel } - } as WorkspaceConfigWithExtras; + }; }) ); return workspaces; } - private workspaceConfigsCache = new Map(); + private workspaceConfigsCache = new Map(); @AddStackToErrorMessage public async onDidChangeConfiguration(args: DidChangeConfigurationParams) { @@ -795,6 +800,7 @@ interface BrightScriptClientConfiguration { configFile: string; languageServer: { enableThreading: boolean; + enableDiscovery: boolean; logLevel: LogLevel | string; }; } @@ -805,11 +811,3 @@ function logAndIgnoreError(error: Error) { } console.error(error); } - -export type WorkspaceConfigWithExtras = WorkspaceConfig & { - bsconfigPath: string; - languageServer: { - enableThreading: boolean; - logLevel: LogLevel | string | undefined; - }; -}; diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index 9508b12b6..e65841f95 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -52,17 +52,23 @@ export class BrsFileValidator { this.validateEnumDeclaration(node); //register this enum declaration - node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance); + if (node.tokens.name) { + node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance); + } }, ClassStatement: (node) => { this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.classKeyword, node.name)); //register this class - node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance); + if (node.name) { + node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance); + } }, AssignmentStatement: (node) => { //register this variable - node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance); + if (node.name) { + node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance); + } }, DottedSetStatement: (node) => { this.validateNoOptionalChainingInVarSet(node, [node.obj]); @@ -77,11 +83,13 @@ export class BrsFileValidator { NamespaceStatement: (node) => { this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.keyword, node.nameExpression)); - node.parent.getSymbolTable().addSymbol( - node.name.split('.')[0], - node.nameExpression.range, - DynamicType.instance - ); + if (node.name) { + node.parent.getSymbolTable().addSymbol( + node.name.split('.')[0], + node.nameExpression.range, + DynamicType.instance + ); + } }, FunctionStatement: (node) => { this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.functionType, node.name)); @@ -101,11 +109,13 @@ export class BrsFileValidator { const funcType = node.func.getFunctionType(); funcType.setName(transpiledNamespaceFunctionName); - this.event.file.parser.ast.symbolTable.addSymbol( - transpiledNamespaceFunctionName, - node.name.range, - funcType - ); + if (node.name) { + this.event.file.parser.ast.symbolTable.addSymbol( + transpiledNamespaceFunctionName, + node.name.range, + funcType + ); + } } }, FunctionExpression: (node) => { @@ -115,17 +125,23 @@ export class BrsFileValidator { this.validateFunctionParameterCount(node); }, FunctionParameterExpression: (node) => { - const paramName = node.name?.text; - const symbolTable = node.getSymbolTable(); - symbolTable?.addSymbol(paramName, node.name.range, node.type); + if (node.name) { + const paramName = node.name.text; + const symbolTable = node.getSymbolTable(); + symbolTable?.addSymbol(paramName, node.name.range, node.type); + } }, InterfaceStatement: (node) => { this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name)); - node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map())); + if (node.tokens.name) { + node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map())); + } }, ConstStatement: (node) => { this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name)); - node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance); + if (node.tokens.name) { + node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance); + } }, CatchStatement: (node) => { node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance); diff --git a/src/lsp/ProjectManager.spec.ts b/src/lsp/ProjectManager.spec.ts index 93a55e561..a38bfa4fe 100644 --- a/src/lsp/ProjectManager.spec.ts +++ b/src/lsp/ProjectManager.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { ProjectManager } from './ProjectManager'; -import { tempDir, rootDir, expectZeroDiagnostics, expectDiagnostics, expectCompletionsIncludes } from '../testHelpers.spec'; +import { tempDir, rootDir, expectZeroDiagnostics, expectDiagnostics, expectCompletionsIncludes, workspaceSettings } from '../testHelpers.spec'; import * as fsExtra from 'fs-extra'; import util, { standardizePath as s } from '../util'; import type { SinonStub } from 'sinon'; @@ -93,9 +93,7 @@ describe('ProjectManager', () => { describe('validation tracking', () => { it('tracks validation state', async () => { - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); const project = manager.projects[0] as Project; //force validation to take a while @@ -127,18 +125,14 @@ describe('ProjectManager', () => { it('finds bsconfig in a folder', async () => { fsExtra.outputFileSync(`${rootDir}/bsconfig.json`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect(manager.projects[0].projectPath).to.eql(s`${rootDir}`); }); it('finds bsconfig at root and also in subfolder', async () => { fsExtra.outputFileSync(`${rootDir}/bsconfig.json`, ''); fsExtra.outputFileSync(`${rootDir}/subdir/bsconfig.json`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath).sort() ).to.eql([ @@ -151,7 +145,7 @@ describe('ProjectManager', () => { fsExtra.outputFileSync(`${rootDir}/bsconfig.json`, ''); fsExtra.outputFileSync(`${rootDir}/subdir/bsconfig.json`, ''); await manager.syncProjects([{ - workspaceFolder: rootDir, + ...workspaceSettings, excludePatterns: ['**/subdir/**/*'] }]); expect( @@ -163,8 +157,23 @@ describe('ProjectManager', () => { it('uses rootDir when manifest found but no brightscript file', async () => { fsExtra.outputFileSync(`${rootDir}/subdir/manifest`, ''); + await manager.syncProjects([workspaceSettings]); + expect( + manager.projects.map(x => x.projectPath) + ).to.eql([ + s`${rootDir}` + ]); + }); + + it('returns root folder when automatic discovery is disabled', async () => { + fsExtra.outputFileSync(`${rootDir}/project1/bsconfig.json`, ''); + fsExtra.outputFileSync(`${rootDir}/project2/bsconfig.json`, ''); await manager.syncProjects([{ - workspaceFolder: rootDir + ...workspaceSettings, + languageServer: { + ...workspaceSettings.languageServer, + enableDiscovery: false + } }]); expect( manager.projects.map(x => x.projectPath) @@ -201,9 +210,7 @@ describe('ProjectManager', () => { end sub `); fsExtra.outputFileSync(`${rootDir}/manifest`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expectDiagnostics(await onNextDiagnostics(), [ DiagnosticMessages.cannotFindName('nameNotDefined').message, 'Test diagnostic' @@ -213,9 +220,7 @@ describe('ProjectManager', () => { it('uses subdir when manifest and brightscript file found', async () => { fsExtra.outputFileSync(`${rootDir}/subdir/manifest`, ''); fsExtra.outputFileSync(`${rootDir}/subdir/source/main.brs`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath) ).to.eql([ @@ -226,9 +231,7 @@ describe('ProjectManager', () => { it('removes stale projects', async () => { fsExtra.outputFileSync(`${rootDir}/subdir1/bsconfig.json`, ''); fsExtra.outputFileSync(`${rootDir}/subdir2/bsconfig.json`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath).sort() ).to.eql([ @@ -237,9 +240,7 @@ describe('ProjectManager', () => { ]); fsExtra.removeSync(`${rootDir}/subdir1/bsconfig.json`); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath).sort() ).to.eql([ @@ -250,9 +251,7 @@ describe('ProjectManager', () => { it('keeps existing projects on subsequent sync calls', async () => { fsExtra.outputFileSync(`${rootDir}/subdir1/bsconfig.json`, ''); fsExtra.outputFileSync(`${rootDir}/subdir2/bsconfig.json`, ''); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath).sort() ).to.eql([ @@ -260,9 +259,7 @@ describe('ProjectManager', () => { s`${rootDir}/subdir2` ]); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager.projects.map(x => x.projectPath).sort() ).to.eql([ @@ -275,9 +272,7 @@ describe('ProjectManager', () => { describe('getCompletions', () => { it('works for quick file changes', async () => { //set up the project - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); //add the namespace first await setFile(s`${rootDir}/source/alpha.bs`, ` @@ -334,9 +329,7 @@ describe('ProjectManager', () => { fsExtra.outputJsonSync(`${rootDir}/project1/bsconfig.json`, { rootDir: rootDir }); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); sinon.stub(manager.projects[0], 'applyFileChanges').returns(Promise.resolve([ //return an undefined item, which used to cause a specific crash @@ -375,9 +368,7 @@ describe('ProjectManager', () => { ] }); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); let deferred1 = new Deferred(); let deferred2 = new Deferred(); @@ -431,9 +422,7 @@ describe('ProjectManager', () => { ] }); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); const stub = sinon.stub(manager as any, 'handleFileChange').callThrough(); @@ -478,9 +467,7 @@ describe('ProjectManager', () => { files: ['source/**/*'] }); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); const stub = sinon.stub(manager['projects'][0], 'applyFileChanges').callThrough(); @@ -505,9 +492,7 @@ describe('ProjectManager', () => { it('does not create a standalone project for files that exist in a known project', async () => { fsExtra.outputFileSync(s`${rootDir}/source/main.brs`, `sub main() : end sub`); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); await onNextDiagnostics(); @@ -522,9 +507,7 @@ describe('ProjectManager', () => { }); it('converts a missing file to a delete', async () => { - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); await onNextDiagnostics(); let applyFileChangesDeferred = new Deferred(); @@ -568,9 +551,7 @@ describe('ProjectManager', () => { it('properly syncs changes', async () => { fsExtra.outputFileSync(`${rootDir}/source/lib1.brs`, `sub test1():print "alpha":end sub`); fsExtra.outputFileSync(`${rootDir}/source/lib2.brs`, `sub test2():print "beta":end sub`); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expectZeroDiagnostics(await onNextDiagnostics()); await manager.handleFileChanges([ @@ -594,9 +575,7 @@ describe('ProjectManager', () => { it('adds all new files in a folder', async () => { fsExtra.outputFileSync(`${rootDir}/source/main.brs`, `sub main():print "main":end sub`); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expectZeroDiagnostics(await onNextDiagnostics()); //add a few files to a folder, then register that folder as an "add" @@ -622,9 +601,7 @@ describe('ProjectManager', () => { fsExtra.outputFileSync(`${rootDir}/source/libs/alpha/charlie/delta.brs`, `sub delta():print two:end sub`); fsExtra.outputFileSync(`${rootDir}/source/libs/echo/foxtrot.brs`, `sub foxtrot():print three:end sub`); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expectDiagnostics(await onNextDiagnostics(), [ DiagnosticMessages.cannotFindName('one').message, @@ -650,8 +627,11 @@ describe('ProjectManager', () => { it('spawns a worker thread when threading is enabled', async () => { await manager.syncProjects([{ - workspaceFolder: rootDir, - enableThreading: true + ...workspaceSettings, + languageServer: { + ...workspaceSettings.languageServer, + enableThreading: true + } }]); expect(manager.projects[0]).instanceof(WorkerThreadProject); }); @@ -659,9 +639,7 @@ describe('ProjectManager', () => { describe('getProject', () => { it('uses .projectPath if param is not a string', async () => { - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); expect( manager['getProject']({ projectPath: rootDir @@ -674,9 +652,7 @@ describe('ProjectManager', () => { describe('createAndActivateProject', () => { it('skips creating project if we already have it', async () => { - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); await manager['createAndActivateProject']({ projectPath: rootDir @@ -715,9 +691,7 @@ describe('ProjectManager', () => { describe('removeProject', () => { it('handles undefined', async () => { manager['removeProject'](undefined); - await manager.syncProjects([{ - workspaceFolder: rootDir - }]); + await manager.syncProjects([workspaceSettings]); manager['removeProject'](undefined); }); @@ -812,7 +786,7 @@ describe('ProjectManager', () => { const host = '127.0.0.1'; //write a small brighterscript plugin to allow this test to communicate with the thread - fsExtra.outputFileSync(`${rootDir}/plugin.js`, ` + fsExtra.outputFileSync(`${rootDir}/pluginsocket.js`, ` ${Plugin.toString()}; exports.default = function() { return new Plugin(${port}, "${host}"); @@ -821,14 +795,17 @@ describe('ProjectManager', () => { //write a bsconfig that will load this plugin fsExtra.outputJsonSync(`${rootDir}/bsconfig.json`, { plugins: [ - `${rootDir}/plugin.js` + `${rootDir}/pluginsocket.js` ] }); //wait for the projects to finish syncing/loading await manager.syncProjects([{ - workspaceFolder: rootDir, - enableThreading: true + ...workspaceSettings, + languageServer: { + ...workspaceSettings.languageServer, + enableThreading: true + } }]); //establish the connection with the plugin @@ -871,10 +848,7 @@ describe('ProjectManager', () => { }); //wait for the projects to finish syncing/loading - await manager.syncProjects([{ - workspaceFolder: rootDir, - enableThreading: false - }]); + await manager.syncProjects([workspaceSettings]); const stub = sinon.stub(manager as any, 'reloadProject').callThrough(); diff --git a/src/lsp/ProjectManager.ts b/src/lsp/ProjectManager.ts index 72f42ff19..8900804e5 100644 --- a/src/lsp/ProjectManager.ts +++ b/src/lsp/ProjectManager.ts @@ -14,7 +14,7 @@ import type { FileChange, MaybePromise } from '../interfaces'; import { BusyStatusTracker } from '../BusyStatusTracker'; import * as fastGlob from 'fast-glob'; import { PathCollection, PathFilterer } from './PathFilterer'; -import type { Logger } from '../logging'; +import type { Logger, LogLevel } from '../logging'; import { createLogger } from '../logging'; import { Cache } from '../Cache'; import { ActionQueue } from './ActionQueue'; @@ -284,7 +284,7 @@ export class ProjectManager { projectPath: s`${projectPath}`, workspaceFolder: s`${workspaceConfig.workspaceFolder}`, excludePatterns: workspaceConfig.excludePatterns, - enableThreading: workspaceConfig.enableThreading + enableThreading: workspaceConfig.languageServer.enableThreading })); }) )).flat(1); @@ -646,6 +646,11 @@ export class ProjectManager { * If none are found, then the workspaceFolder itself is treated as a project */ private async getProjectPaths(workspaceConfig: WorkspaceConfig) { + //automatic discovery disabled? + if (!workspaceConfig.languageServer.enableDiscovery) { + return [workspaceConfig.workspaceFolder]; + } + //get the list of exclude patterns, negate them so they actually work like excludes), and coerce to forward slashes since that's what fast-glob expects const excludePatterns = (workspaceConfig.excludePatterns ?? []).map(x => s`!${x}`.replace(/[\\/]+/g, '/')); @@ -868,9 +873,22 @@ export interface WorkspaceConfig { */ bsconfigPath?: string; /** - * Should the projects in this workspace be run in their own dedicated worker threads, or all run on the main thread + * Language server configuration options */ - enableThreading?: boolean; + languageServer: { + /** + * Should the projects in this workspace be run in their own dedicated worker threads, or all run on the main thread + */ + enableThreading: boolean; + /** + * Should the language server automatically discover projects in this workspace? + */ + enableDiscovery: boolean; + /** + * The log level to use for this workspace + */ + logLevel?: LogLevel | string; + }; } interface StandaloneProject extends LspProject { diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 30c127036..bf5183d9d 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -13,11 +13,19 @@ import type { CodeWithSourceMap } from 'source-map'; import { getDiagnosticLine } from './diagnosticUtils'; import { firstBy } from 'thenby'; import undent from 'undent'; +import type { WorkspaceConfig } from './lsp/ProjectManager'; export const cwd = s`${__dirname}/../`; export const tempDir = s`${__dirname}/../.tmp`; export const rootDir = s`${tempDir}/rootDir`; export const stagingDir = s`${tempDir}/stagingDir`; +export const workspaceSettings: WorkspaceConfig = { + workspaceFolder: rootDir, + languageServer: { + enableThreading: false, + enableDiscovery: true + } +}; export const trim = undent; const sinon = createSandbox(); diff --git a/src/util.ts b/src/util.ts index def03f511..e02387863 100644 --- a/src/util.ts +++ b/src/util.ts @@ -905,7 +905,9 @@ export class Util { * If the two items both start on the same line */ public sameStartLine(first: { range: Range }, second: { range: Range }) { - if (first && second && first.range.start.line === second.range.start.line) { + if (first && second && (first.range !== undefined) && (second.range !== undefined) && + first.range.start.line === second.range.start.line + ) { return true; } else { return false;