diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts index 004e3a2f3..681ef1923 100644 --- a/src/LanguageServer.ts +++ b/src/LanguageServer.ts @@ -345,7 +345,7 @@ export class LanguageServer { @AddStackToErrorMessage private async onTextDocumentDidChangeContent(event: TextDocumentChangeEvent) { - this.logger.log('onTextDocumentDidChangeContent', event.document.uri); + this.logger.debug('onTextDocumentDidChangeContent', event.document.uri); await this.projectManager.handleFileChanges([{ srcPath: URI.parse(event.document.uri).fsPath, @@ -400,10 +400,14 @@ export class LanguageServer { * Provide a list of completion items based on the current cursor position */ @AddStackToErrorMessage - public async onCompletion(params: CompletionParams, token: CancellationToken, workDoneProgress: WorkDoneProgressReporter, resultProgress: ResultProgressReporter): Promise { - this.logger.log('onCompletion', params, token); + public async onCompletion(params: CompletionParams, cancellationToken: CancellationToken, workDoneProgress: WorkDoneProgressReporter, resultProgress: ResultProgressReporter): Promise { + this.logger.info('onCompletion', params, cancellationToken); const srcPath = util.uriToPath(params.textDocument.uri); - const completions = await this.projectManager.getCompletions({ srcPath: srcPath, position: params.position }); + const completions = await this.projectManager.getCompletions({ + srcPath: srcPath, + position: params.position, + cancellationToken: cancellationToken + }); return completions; } diff --git a/src/Program.spec.ts b/src/Program.spec.ts index 6ce4c7124..a568e879b 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -935,6 +935,24 @@ describe('Program', () => { }]); }); + it('finds enum member after dot in if statement', () => { + program.setFile('source/main.bs', ` + sub test() + if alpha.beta. then + end if + end sub + namespace alpha.beta + const isEnabled = true + end namespace + `); + program.validate(); + const completions = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(2, 34)); + expect(completions.map(x => ({ kind: x.kind, label: x.label }))).to.eql([{ + label: 'isEnabled', + kind: CompletionItemKind.Constant + }]); + }); + it('includes `for` variable', () => { program.setFile('source/main.brs', ` sub main() diff --git a/src/lsp/DocumentManager.spec.ts b/src/lsp/DocumentManager.spec.ts index 16798983e..e67bec92c 100644 --- a/src/lsp/DocumentManager.spec.ts +++ b/src/lsp/DocumentManager.spec.ts @@ -1,7 +1,8 @@ import { expect } from 'chai'; -import util from '../util'; -import type { DocumentAction } from './DocumentManager'; +import { util, standardizePath as s } from '../util'; +import type { DocumentAction, SetDocumentAction } from './DocumentManager'; import { DocumentManager } from './DocumentManager'; +import { rootDir } from '../testHelpers.spec'; describe('DocumentManager', () => { let manager: DocumentManager; @@ -36,6 +37,36 @@ describe('DocumentManager', () => { }]); }); + it('does not lose newly added that arrives during a flush operation', async () => { + const srcPath = s`${rootDir}/source/main.bs`; + + let contentsQueue = [ + 'two', + 'three', + 'four' + ]; + manager = new DocumentManager({ + delay: 5, + flushHandler: (event) => { + //once the flush happens, add NEW data to the queue. this is the data we need to ensure we don't lose + if (contentsQueue.length > 0) { + manager.set({ srcPath: srcPath, fileContents: contentsQueue.shift() }); + } + + //store the actions + results.push(...event.actions); + } + }); + manager.set({ srcPath: srcPath, fileContents: 'one' }); + await manager.onIdle(); + expect(results.map(x => (x as SetDocumentAction).fileContents)).to.eql([ + 'one', + 'two', + 'three', + 'four' + ]); + }); + it('any file change delays the first one', async () => { manager.set({ srcPath: 'alpha', fileContents: 'one' }); await util.sleep(1); diff --git a/src/lsp/DocumentManager.ts b/src/lsp/DocumentManager.ts index d9d6d3499..e2580fe76 100644 --- a/src/lsp/DocumentManager.ts +++ b/src/lsp/DocumentManager.ts @@ -80,8 +80,8 @@ export class DocumentManager { const event: FlushEvent = { actions: [...this.queue.values()] }; - await this.options.flushHandler?.(event); this.queue.clear(); + await this.options.flushHandler?.(event); } catch (e) { console.error(e); } diff --git a/src/lsp/Project.ts b/src/lsp/Project.ts index 592b160dc..f033a1c01 100644 --- a/src/lsp/Project.ts +++ b/src/lsp/Project.ts @@ -208,7 +208,10 @@ export class Project implements LspProject { * This will cancel any pending validation cycles and queue a future validation cycle instead. */ public async applyFileChanges(documentActions: DocumentAction[]): Promise { + this.logger.debug('project.applyFileChanges', documentActions.map(x => x.srcPath)); + await this.onIdle(); + let didChangeFiles = false; const result = [...documentActions] as DocumentActionWithStatus[]; // eslint-disable-next-line @typescript-eslint/prefer-for-of @@ -251,6 +254,9 @@ export class Project implements LspProject { //trigger a validation (but don't wait for it. That way we can cancel it sooner if we get new incoming data or requests) void this.validate(); } + + this.logger.debug('project.applyFileChanges done', documentActions.map(x => x.srcPath)); + return result; } @@ -383,6 +389,7 @@ export class Project implements LspProject { public async getCodeActions(options: { srcPath: string; range: Range }) { await this.onIdle(); + if (this.builder.program.hasFile(options.srcPath)) { const codeActions = this.builder.program.getCodeActions(options.srcPath, options.range); //clone each diagnostic since certain diagnostics can have circular reference properties that kill the language server if serialized @@ -397,6 +404,9 @@ export class Project implements LspProject { public async getCompletions(options: { srcPath: string; position: Position }): Promise { await this.onIdle(); + + this.logger.debug('project.getCompletions', options.srcPath, options.position); + if (this.builder.program.hasFile(options.srcPath)) { const completions = this.builder.program.getCompletions(options.srcPath, options.position); const result = CompletionList.create(completions); diff --git a/src/lsp/ProjectManager.ts b/src/lsp/ProjectManager.ts index e63bdc346..7e0c3ada9 100644 --- a/src/lsp/ProjectManager.ts +++ b/src/lsp/ProjectManager.ts @@ -6,7 +6,7 @@ import type { LspDiagnostic, LspProject, ProjectConfig } from './LspProject'; import { Project } from './Project'; import { WorkerThreadProject } from './worker/WorkerThreadProject'; import { FileChangeType } from 'vscode-languageserver-protocol'; -import type { Hover, Position, Range, Location, SignatureHelp, DocumentSymbol, SymbolInformation, WorkspaceSymbol, CompletionList } from 'vscode-languageserver-protocol'; +import type { Hover, Position, Range, Location, SignatureHelp, DocumentSymbol, SymbolInformation, WorkspaceSymbol, CompletionList, CancellationToken } from 'vscode-languageserver-protocol'; import { Deferred } from '../deferred'; import type { DocumentActionWithStatus, FlushEvent } from './DocumentManager'; import { DocumentManager } from './DocumentManager'; @@ -73,6 +73,7 @@ export class ProjectManager { @TrackBusyStatus private async flushDocumentChanges(event: FlushEvent) { this.logger.log('flushDocumentChanges', event.actions.map(x => x.srcPath)); + //ensure that we're fully initialized before proceeding await this.onInitialized(); @@ -102,7 +103,9 @@ export class ProjectManager { const projectActions = actions.filter(action => { return action.type === 'delete' || filterer.isMatch(action.srcPath); }); - return project.applyFileChanges(projectActions); + if (projectActions.length > 0) { + return project.applyFileChanges(projectActions); + } })); //create standalone projects for any files not handled by any project @@ -189,6 +192,8 @@ export class ProjectManager { //There are race conditions where the fileChangesQueue will become idle, but that causes the documentManager //to start a new flush. So we must keep waiting until everything is idle while (!this.documentManager.isIdle || !this.fileChangesQueue.isIdle) { + this.logger.debug('onIdle', { documentManagerIdle: this.documentManager.isIdle, fileChangesQueueIdle: this.fileChangesQueue.isIdle }); + await Promise.allSettled([ //make sure all pending file changes have been flushed this.documentManager.onIdle(), @@ -196,6 +201,8 @@ export class ProjectManager { this.fileChangesQueue.onIdle() ]); } + + this.logger.info('onIdle debug', { documentManagerIdle: this.documentManager.isIdle, fileChangesQueueIdle: this.fileChangesQueue.isIdle }); } /** @@ -398,9 +405,15 @@ export class ProjectManager { * Get the completions for the given position in the file */ @TrackBusyStatus - public async getCompletions(options: { srcPath: string; position: Position }): Promise { + public async getCompletions(options: { srcPath: string; position: Position; cancellationToken?: CancellationToken }): Promise { await this.onIdle(); + //if the request has been cancelled since originall requested due to idle time being slow, skip the rest of the wor + if (options?.cancellationToken?.isCancellationRequested) { + this.logger.log('ProjectManager getCompletions cancelled', options); + return; + } + this.logger.log('ProjectManager getCompletions', options); //Ask every project for results, keep whichever one responds first that has a valid response let result = await util.promiseRaceMatch(