diff --git a/Composer/packages/lib/code-editor/src/BaseEditor.tsx b/Composer/packages/lib/code-editor/src/BaseEditor.tsx index 85fcb7c1aa..4ec24df036 100644 --- a/Composer/packages/lib/code-editor/src/BaseEditor.tsx +++ b/Composer/packages/lib/code-editor/src/BaseEditor.tsx @@ -31,6 +31,7 @@ const defaultOptions = { folding: false, renderLineHighlight: 'none', formatOnType: true, + fixedOverflowWidgets: true, }; const styles = { 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 cedb22bfd7..fc12cca6dd 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts @@ -35,6 +35,7 @@ import { updateTemplate, cardTypes, cardPropDict, + cardPropPossibleValueType, } from './utils'; // define init methods call from client @@ -344,7 +345,8 @@ export class LGServer { const position = params.position; const range = Range.create(0, 0, position.line, position.character); const lines = document.getText(range).split('\n'); - let lastLine = ''; + const keyValueRegex = /.+=.+/; + let cardName = ''; for (const line of lines) { if (line.trim().startsWith('#')) { state.push(TEMPLATENAME); @@ -352,14 +354,53 @@ export class LGServer { state.push(TEMPLATEBODY); } else if ((state[state.length - 1] === TEMPLATENAME || templateId) && line.trim().startsWith('[')) { state.push(STRUCTURELG); - lastLine = line; - } else if (state[state.length - 1] === STRUCTURELG && line.trim() === '') { - return lastLine.trim().substring(1); + cardName = line.trim().substring(1); + } else if (state[state.length - 1] === STRUCTURELG && (line.trim() === '' || keyValueRegex.test(line))) { + state.push(STRUCTURELG); } else { state.push(ROOT); } } + if (state[state.length - 1] === STRUCTURELG) { + return cardName; + } + + return undefined; + } + + private findLastStructureLGProps( + params: TextDocumentPositionParams, + templateId: string | undefined + ): string[] | undefined { + const state: LGCursorState[] = []; + const document = this.documents.get(params.textDocument.uri); + if (!document) return; + const position = params.position; + const range = Range.create(0, 0, position.line, position.character); + const lines = document.getText(range).split('\n'); + const keyValueRegex = /.+=.+/; + let props: string[] = []; + for (const line of lines) { + if (line.trim().startsWith('#')) { + state.push(TEMPLATENAME); + } else if (line.trim().startsWith('-')) { + state.push(TEMPLATEBODY); + } else if ((state[state.length - 1] === TEMPLATENAME || templateId) && line.trim().startsWith('[')) { + state.push(STRUCTURELG); + props = []; + } else if (state[state.length - 1] === STRUCTURELG && (line.trim() === '' || keyValueRegex.test(line))) { + state.push(STRUCTURELG); + props.push(line.split('=')[0].trim()); + } else { + state.push(ROOT); + } + } + + if (state[state.length - 1] === STRUCTURELG) { + return props; + } + return undefined; } @@ -504,6 +545,7 @@ export class LGServer { const lgDoc = this.getLGDocument(document); const lgFile = lgDoc?.index(); const templateId = lgDoc?.templateId; + const lines = document.getText(range).split('\n'); if (!lgFile) { return Promise.resolve(null); } @@ -552,59 +594,55 @@ export class LGServer { } const cardType = this.matchCardTypeState(params, templateId); + const propsList = this.findLastStructureLGProps(params, templateId); + const cardNameRegex = /^\s*\[[\w]+/; + const lastLine = lines[lines.length - 2]; + const paddingIndent = cardNameRegex.test(lastLine) ? '\t' : ''; + const normalCardTypes = ['CardAction', 'Suggestions', 'Attachment']; if (cardType && cardTypes.includes(cardType)) { - let item: CompletionItem | undefined = undefined; - if (cardType === 'CardAction') { - let insertStr = ''; - insertStr = cardPropDict[cardType].map(u => `\t${u} = `).join('\r\n'); - item = { - label: `Properties for ${cardType}`, - kind: CompletionItemKind.Keyword, - insertText: insertStr, - documentation: `Suggested properties for Card: ${cardType}`, - }; - } else if (cardType === 'Suggestions') { - let insertStr = ''; - insertStr = cardPropDict[cardType].map(u => `\t${u} = `).join('\r\n'); - item = { - label: `Properties for ${cardType}`, - kind: CompletionItemKind.Keyword, - insertText: insertStr, - documentation: `Suggested properties for Card: ${cardType}`, - }; - } else if (cardType === 'Attachment') { - let insertStr = ''; - insertStr = cardPropDict[cardType].map(u => `\t${u} = `).join('\r\n'); - item = { - label: `Properties for ${cardType}`, - kind: CompletionItemKind.Keyword, - insertText: insertStr, - documentation: `Suggested properties for Card: ${cardType}`, - }; + const items: CompletionItem[] = []; + if (normalCardTypes.includes(cardType)) { + cardPropDict[cardType].forEach(u => { + if (!propsList?.includes(u)) { + const item = { + label: `${u}: ${cardPropPossibleValueType[u]}`, + kind: CompletionItemKind.Snippet, + insertText: `${paddingIndent}${u} = ${cardPropPossibleValueType[u]}`, + documentation: `Suggested propertiy ${u} in ${cardType}`, + }; + items.push(item); + } + }); } else if (cardType.endsWith('Card')) { - let insertStr = ''; - insertStr = cardPropDict.Cards.map(u => `\t${u} = `).join('\r\n'); - item = { - label: `Properties for ${cardType}`, - kind: CompletionItemKind.Keyword, - insertText: insertStr, - documentation: `Suggested properties for Card: ${cardType}`, - }; + cardPropDict.Cards.forEach(u => { + if (!propsList?.includes(u)) { + const item = { + label: `${u}: ${cardPropPossibleValueType[u]}`, + kind: CompletionItemKind.Snippet, + insertText: `${paddingIndent}${u} = ${cardPropPossibleValueType[u]}`, + documentation: `Suggested propertiy ${u} in ${cardType}`, + }; + items.push(item); + } + }); } else { - let insertStr = ''; - insertStr = cardPropDict.Others.map(u => `\t${u} = `).join('\r\n'); - item = { - label: `Properties for ${cardType}`, - kind: CompletionItemKind.Keyword, - insertText: insertStr, - documentation: `Suggested properties for Card: ${cardType}`, - }; + cardPropDict.Others.forEach(u => { + if (!propsList?.includes(u)) { + const item = { + label: `${u}: ${cardPropPossibleValueType[u]}`, + kind: CompletionItemKind.Snippet, + insertText: `${paddingIndent}${u} = ${cardPropPossibleValueType[u]}`, + documentation: `Suggested propertiy ${u} in ${cardType}`, + }; + items.push(item); + } + }); } - if (item) { + if (items.length > 0) { return Promise.resolve({ isIncomplete: true, - items: [item], + items: items, }); } } @@ -639,8 +677,29 @@ export class LGServer { const range = Range.create(0, 0, position.line, position.character); const lines = document.getText(range).split('\n'); const isInStructureLGMode = this.matchStructureLG(lines); - if (key === '\n' && isInStructureLGMode) { - const deleteRange = Range.create(position.line, 0, position.line, 4); + const lgDoc = this.getLGDocument(document); + const templateId = lgDoc?.templateId; + const cardType = this.matchCardTypeState(params, templateId); + const propsList = this.findLastStructureLGProps(params, templateId); + const normalCardTypes = ['CardAction', 'Suggestions', 'Attachment']; + let expectedPropsList: string[] = []; + let matchedAllProps = false; + if (cardType) { + if (normalCardTypes.includes(cardType)) { + expectedPropsList = cardPropDict[cardType]; + } else if (cardType.endsWith('Card')) { + expectedPropsList = cardPropDict.Cards; + } else { + expectedPropsList = cardPropDict.Others; + } + } + + if (expectedPropsList.length > 0 && propsList !== undefined && this.isSubList(expectedPropsList, propsList)) { + matchedAllProps = true; + } + if (key === '\n' && isInStructureLGMode && matchedAllProps) { + const length = templateId ? 4 : 2; + const deleteRange = Range.create(position.line, 0, position.line, length); const deleteItem: TextEdit = TextEdit.del(deleteRange); if (document.getText(deleteRange).trim() === '') { edits.push(deleteItem); @@ -671,6 +730,16 @@ export class LGServer { return true; } + private isSubList(list1: string[], list2: string[]): boolean { + for (const item of list1) { + if (!list2.includes(item)) { + return false; + } + } + + return true; + } + protected validate(document: TextDocument): void { this.cleanPendingValidation(document); this.pendingValidationRequests.set( diff --git a/Composer/packages/tools/language-servers/language-generation/src/utils.ts b/Composer/packages/tools/language-servers/language-generation/src/utils.ts index 5f88ecd386..b9a79c6965 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/utils.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/utils.ts @@ -153,6 +153,20 @@ export const cardTypes = [ 'Activity', ]; +export const cardPropPossibleValueType = { + title: 'An Example Card', + type: 'Action Type', + value: 'Some Value', + SuggestionActions: 'Text | ${Some_CardAction}', + subtitle: 'An Example Subtitle', + text: 'Some text', + image: 'https://example.com/demo.jpg', + buttons: 'Text | ${Some_CardAction}', + contenttype: 'adaptivecard', + content: '${json(fromFile("../../card.json"))}', + name: 'An Example Name', +}; + export const cardPropDict = { CardAction: ['title', 'type', 'value'], Suggestions: ['SuggestionActions'],