diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index cc053ee425..473da6fea7 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -79,3 +79,5 @@ export interface LgFile { } export type FileResolver = (id: string) => FileInfo | undefined; + +export type MemoryResolver = (id: string) => string[] | undefined; diff --git a/Composer/packages/server/src/server.ts b/Composer/packages/server/src/server.ts index defec8b881..4a514bbc22 100644 --- a/Composer/packages/server/src/server.ts +++ b/Composer/packages/server/src/server.ts @@ -114,13 +114,13 @@ const wss: ws.Server = new ws.Server({ perMessageDeflate: false, }); -const { fileResolver } = BotProjectService; +const { fileResolver, staticMemoryResolver } = BotProjectService; function launchLanguageServer(socket: rpc.IWebSocket) { const reader = new rpc.WebSocketMessageReader(socket); const writer = new rpc.WebSocketMessageWriter(socket); const connection: IConnection = createConnection(reader, writer); - const server = new LGServer(connection, fileResolver); + const server = new LGServer(connection, fileResolver, staticMemoryResolver); server.start(); } diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts index fe26dc5b28..ee396cd750 100644 --- a/Composer/packages/server/src/services/project.ts +++ b/Composer/packages/server/src/services/project.ts @@ -38,6 +38,19 @@ export class BotProjectService { return BotProjectService.currentBotProject?.files.find(file => file.name === name); } + public static staticMemoryResolver(name: string): string[] | undefined { + return [ + 'this.value', + 'this.turnCount', + 'turn.DialogEvent.value', + 'intent.score', + 'turn.recognized.intent', + 'turn.recognized.intents.***.score', + 'turn.recognized.score', + 'turn.recognized.entities', + ]; + } + public static getCurrentBotProject(): BotProject | undefined { BotProjectService.initialize(); return BotProjectService.currentBotProject; diff --git a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts index 4308de561e..de4d7784c4 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts @@ -18,7 +18,7 @@ import { } from 'vscode-languageserver-types'; import { TextDocumentPositionParams } from 'vscode-languageserver-protocol'; import get from 'lodash/get'; -import { lgIndexer, filterTemplateDiagnostics, isValid, FileResolver, FileInfo } from '@bfc/indexers'; +import { lgIndexer, filterTemplateDiagnostics, isValid, FileResolver, FileInfo, MemoryResolver } from '@bfc/indexers'; import { buildInfunctionsMap } from './builtinFunctionsMap'; import { @@ -43,8 +43,13 @@ export class LGServer { protected readonly documents = new TextDocuments(); protected readonly pendingValidationRequests = new Map(); protected LGDocuments: LGDocument[] = []; + private memoryVariables: Record = {}; - constructor(protected readonly connection: IConnection, protected readonly resolver?: FileResolver) { + constructor( + protected readonly connection: IConnection, + protected readonly resolver?: FileResolver, + protected readonly memoryResolver?: MemoryResolver + ) { this.documents.listen(this.connection); this.documents.onDidChangeContent(change => this.validate(change.document)); this.documents.onDidClose(event => { @@ -65,6 +70,7 @@ export class LGServer { codeActionProvider: false, completionProvider: { resolveProvider: true, + triggerCharacters: ['.'], }, hoverProvider: true, foldingRangeProvider: false, @@ -91,6 +97,41 @@ export class LGServer { this.connection.listen(); } + protected updateObject(propertyList: string[]): void { + let tempVariable: Record = this.memoryVariables; + const antPattern = /\*+/; + const normalizedAnyPattern = '***'; + for (let property of propertyList) { + if (property in tempVariable) { + tempVariable = tempVariable[property]; + } else { + if (antPattern.test(property)) { + property = normalizedAnyPattern; + } + tempVariable[property] = {}; + tempVariable = tempVariable[property]; + } + } + } + + protected updateMemoryVariables(uri: string): void { + if (!this.memoryResolver) { + return; + } + + const memoryFileInfo: string[] | undefined = this.memoryResolver(uri); + if (!memoryFileInfo || memoryFileInfo.length === 0) { + return; + } + + memoryFileInfo.forEach(variable => { + const propertyList = variable.split('.'); + if (propertyList.length >= 1) { + this.updateObject(propertyList); + } + }); + } + protected validateLgOption(document: TextDocument, lgOption?: LGOption) { if (!lgOption) return; @@ -281,11 +322,82 @@ export class LGServer { return state.pop(); } + protected matchingCompletionProperty(propertyList: string[], ...objects: object[]): CompletionItem[] { + const completionList: CompletionItem[] = []; + const normalizedAnyPattern = '***'; + for (const obj of objects) { + let tempVariable = obj; + for (const property of propertyList) { + if (property in tempVariable) { + tempVariable = tempVariable[property]; + } else if (normalizedAnyPattern in tempVariable) { + tempVariable = tempVariable[normalizedAnyPattern]; + } else { + tempVariable = {}; + } + } + + if (!tempVariable || Object.keys(tempVariable).length === 0) { + continue; + } + + Object.keys(tempVariable).forEach(e => { + if (e.toString() !== normalizedAnyPattern) { + const item = { + label: e.toString(), + kind: CompletionItemKind.Property, + insertText: e.toString(), + documentation: '', + }; + if (!completionList.includes(item)) { + completionList.push(item); + } + } + }); + } + + return completionList; + } + + protected findValidMemoryVariables(params: TextDocumentPositionParams): CompletionItem[] { + const document = this.documents.get(params.textDocument.uri); + if (!document) return []; + const position = params.position; + const range = getRangeAtPosition(document, position); + const wordAtCurRange = document.getText(range); + const endWithDot = wordAtCurRange.endsWith('.'); + + this.updateMemoryVariables(params.textDocument.uri); + const memoryVariblesRootCompletionList = Object.keys(this.memoryVariables).map(e => { + return { + label: e.toString(), + kind: CompletionItemKind.Property, + insertText: e.toString(), + documentation: '', + }; + }); + + if (!wordAtCurRange || !endWithDot) { + return memoryVariblesRootCompletionList; + } + + let propertyList = wordAtCurRange.split('.'); + propertyList = propertyList.slice(0, propertyList.length - 1); + + const completionList = this.matchingCompletionProperty(propertyList, this.memoryVariables); + + return completionList; + } + protected completion(params: TextDocumentPositionParams): Thenable { const document = this.documents.get(params.textDocument.uri); if (!document) { return Promise.resolve(null); } + const position = params.position; + const range = getRangeAtPosition(document, position); + const wordAtCurRange = document.getText(range); + const endWithDot = wordAtCurRange.endsWith('.'); const lgFile = this.getLGDocument(document)?.index(); if (!lgFile) { return Promise.resolve(null); @@ -312,9 +424,21 @@ export class LGServer { }; }); + const completionPropertyResult = this.findValidMemoryVariables(params); + const matchedState = this.matchState(params); if (matchedState === EXPRESSION) { - return Promise.resolve({ isIncomplete: true, items: completionTemplateList.concat(completionFunctionList) }); + if (endWithDot) { + return Promise.resolve({ + isIncomplete: true, + items: completionPropertyResult, + }); + } else { + return Promise.resolve({ + isIncomplete: true, + items: completionTemplateList.concat(completionFunctionList.concat(completionPropertyResult)), + }); + } } else { return Promise.resolve(null); }