import * as parser from "jsonc-parser" ; import * as vscode from "vscode" ; import { Constants } from "../common/constants" ; import { DigitalTwinConstants } from "./digitalTwinConstants" ; import { ClassNode , PropertyNode , ValueSchema } from "./digitalTwinGraph" ; import { IntelliSenseUtility , JsonNodeType , PropertyPair , ModelContent } from "./intelliSenseUtility" ; import { LANGUAGE_CODE } from "./languageCode" ; /**
 * Diagnostic problem
 */ interface Suggestion { isProperty : boolean ; label : string ; insertText : string ; } /**
 * Completion item provider for DigitalTwin IntelliSense
 */ export class DigitalTwinCompletionItemProvider implements vscode . CompletionItemProvider { param document param position private static completeTextForParse ( document , position ) { const text = document . getText ( ) ; const offset = document . offsetAt ( position ) ; if ( text [ offset ] === Constants . COMPLETION_TRIGGER ) { const edit = { offset , length : 1 , content : Constants . COMPLETION_TRIGGER + Constants . DEFAULT_SEPARATOR } ; return parser . applyEdits ( text , [ edit ] ) ; } return text ; } param suggestion param position param range param separator private static createCompletionItem ( suggestion , position , range , separator ) { const completionItem = { label : suggestion . label , kind : suggestion . isProperty ? vscode . CompletionItemKind . Property : vscode . CompletionItemKind . Value , insertText : new vscode . SnippetString ( suggestion . insertText + separator ) , range : new vscode . Range ( position , range . end ) } ; if ( position . isAfter ( range . start ) ) { completionItem . additionalTextEdits = [ vscode . TextEdit . delete ( new vscode . Range ( range . start , position ) ) ] ; } return completionItem ; } param document param position param node private static evaluateOverwriteRange ( document , position , node ) { let range ; if ( node . type === JsonNodeType . String || node . type === JsonNodeType . Number || node . type === JsonNodeType . Boolean ) { range = IntelliSenseUtility . getNodeRange ( document , node ) ; } else { const word = DigitalTwinCompletionItemProvider . getCurrentWord ( document , position ) ; const start = document . offsetAt ( position ) - word . length ; range = new vscode . Range ( document . positionAt ( start ) , position ) ; } return range ; } param document param position private static getCurrentWord ( document , position ) { let i = position . character - 1 ; const text = document . lineAt ( position . line ) . text ; while ( i >= 0 && DigitalTwinConstants . WORD_STOP . indexOf ( text . charAt ( i ) ) === - 1 ) { i -- ; } return text . substring ( i + 1 , position . character ) ; } param text param offset private static evaluateSeparatorAfter ( text , offset ) { const scanner = parser . createScanner ( text , true ) ; scanner . setPosition ( offset ) ; const token = scanner . scan ( ) ; switch ( token ) { case parser . SyntaxKind . CommaToken : case parser . SyntaxKind . CloseBraceToken : case parser . SyntaxKind . CloseBracketToken : case parser . SyntaxKind . EOF : return Constants . EMPTY_STRING ; default : return Constants . DEFAULT_SEPARATOR ; } } param version param node param includeValue param suggestions private static suggestProperty ( version , node , includeValue , suggestions ) { const exist = new Set < string > ( ) ; let classNode = DigitalTwinCompletionItemProvider . getObjectType ( version , node , exist ) ; if ( classNode && ! IntelliSenseUtility . isAvailableByVersion ( version , classNode . version ) ) { classNode = undefined ; } let dummyNode ; if ( ! classNode ) { if ( ! exist . has ( DigitalTwinConstants . TYPE ) ) { dummyNode = { id : DigitalTwinConstants . TYPE } ; suggestions . push ( { isProperty : true , label : `${ dummyNode . id } ${ DigitalTwinConstants . REQUIRED_PROPERTY_LABEL }` , insertText : DigitalTwinCompletionItemProvider . getInsertTextForProperty ( dummyNode , includeValue ) } ) ; } } else if ( IntelliSenseUtility . isLanguageNode ( classNode ) ) { const stringValueSchema = { id : ValueSchema . String } ; dummyNode = { id : Constants . EMPTY_STRING , range : [ stringValueSchema ] } ; for ( const code of LANGUAGE_CODE ) { if ( exist . has ( code ) ) { continue ; } dummyNode . id = code ; suggestions . push ( { isProperty : true , label : code , insertText : DigitalTwinCompletionItemProvider . getInsertTextForProperty ( dummyNode , includeValue ) } ) ; } } else { const required = classNode . constraint && classNode . constraint . required ? new Set < string > ( classNode . constraint . required ) : new Set < string > ( ) ; for ( const property of IntelliSenseUtility . getPropertiesOfClassByVersion ( classNode , version ) ) { if ( ! property . label || exist . has ( property . label ) ) { continue ; } suggestions . push ( { isProperty : true , label : DigitalTwinCompletionItemProvider . formatLabel ( property . label , required ) , insertText : DigitalTwinCompletionItemProvider . getInsertTextForProperty ( property , includeValue ) } ) ; } DigitalTwinCompletionItemProvider . suggestReservedProperty ( includeValue , exist , required , suggestions ) ; required . clear ( ) ; } exist . clear ( ) ; } param version param node param exist private static getObjectType ( version , node , exist ) { const parent = node . parent ; if ( ! parent || parent . type !== JsonNodeType . Object || ! parent . children ) { return undefined ; } let propertyName ; let objectType ; let propertyPair ; for ( const child of parent . children ) { if ( child === node ) { continue ; } propertyPair = IntelliSenseUtility . parseProperty ( child ) ; if ( ! propertyPair ) { continue ; } propertyName = propertyPair . name . value as string ; exist . add ( propertyName ) ; if ( propertyName === DigitalTwinConstants . TYPE ) { const propertyValue = propertyPair . value ; if ( propertyValue . type === JsonNodeType . String ) { objectType = IntelliSenseUtility . getClasNode ( propertyValue . value as string ) ; } else if ( propertyValue . type === JsonNodeType . Array && propertyValue . children ) { for ( const child of propertyValue . children ) { if ( child . type !== JsonNodeType . String ) { continue ; } const type = child . value as string ; if ( type && DigitalTwinConstants . SUPPORT_SEMANTIC_TYPES . has ( type ) ) { objectType = IntelliSenseUtility . getClasNode ( type ) ; } } } } } if ( ! objectType ) { const propertyNode = DigitalTwinCompletionItemProvider . getOuterPropertyNode ( parent ) ; if ( propertyNode ) { const classes = IntelliSenseUtility . getObjectClasses ( propertyNode , version ) ; if ( classes . length === 1 ) { objectType = classes [ 0 ] ; } } } return objectType ; } param node private static getOuterPropertyNode ( node ) { const propertyPair = IntelliSenseUtility . getOuterPropertyPair ( node ) ; if ( ! propertyPair ) { return undefined ; } const propertyName = IntelliSenseUtility . resolvePropertyName ( propertyPair ) ; return IntelliSenseUtility . getPropertyNode ( propertyName ) ; } param label param required private static formatLabel ( label , required ) { return required . has ( label ) ? `${ label } ${ DigitalTwinConstants . REQUIRED_PROPERTY_LABEL }` : label ; } param includeValue param exist param required param suggestions private static suggestReservedProperty ( includeValue , exist , required , suggestions ) { const properties = [ ] ; const propertyNode = IntelliSenseUtility . getPropertyNode ( DigitalTwinConstants . ID ) ; if ( propertyNode ) { properties . push ( propertyNode ) ; } if ( required . has ( DigitalTwinConstants . TYPE ) ) { properties . push ( { id : DigitalTwinConstants . TYPE } ) ; } for ( const property of properties ) { if ( exist . has ( property . id ) ) { continue ; } suggestions . push ( { isProperty : true , label : DigitalTwinCompletionItemProvider . formatLabel ( property . id , required ) , insertText : DigitalTwinCompletionItemProvider . getInsertTextForProperty ( property , includeValue ) } ) ; } properties . length = 0 ; } param propertyNode param includeValue private static getInsertTextForProperty ( propertyNode , includeValue ) { const name = propertyNode . label || propertyNode . id ; if ( ! includeValue ) { return name ; } let value = "$1" ; if ( propertyNode . isArray ) { value = "[$1]" ; } else if ( propertyNode . range && propertyNode . range . length === 1 ) { const classNode = propertyNode . range [ 0 ] ; if ( IntelliSenseUtility . isObjectClass ( classNode ) ) { value = "{$1}" ; } else if ( ! classNode . label ) { switch ( classNode . id ) { case ValueSchema . String : value = '"$1"' ; break ; case ValueSchema . Int : value = "${1:0}" ; break ; case ValueSchema . Boolean : value = "${1:false}" ; break ; default : } } } return `"${ name }": ${ value }` ; } param version param node param suggestions private static suggestValue ( version , node , suggestions ) { const propertyPair = IntelliSenseUtility . parseProperty ( node ) ; if ( ! propertyPair ) { return ; } let propertyNode ; let propertyName = propertyPair . name . value as string ; if ( propertyName === DigitalTwinConstants . TYPE ) { if ( ! node . parent ) { return ; } propertyNode = DigitalTwinCompletionItemProvider . getOuterPropertyNode ( node . parent ) || IntelliSenseUtility . getEntryNode ( ) ; if ( propertyNode ) { let value ; const classes = IntelliSenseUtility . getObjectClasses ( propertyNode , version ) ; for ( const classNode of classes ) { value = IntelliSenseUtility . getClassType ( classNode ) ; suggestions . push ( { isProperty : false , label : value , insertText : `"${ value }"` } ) ; } } } else { propertyName = IntelliSenseUtility . resolvePropertyName ( propertyPair ) ; propertyNode = IntelliSenseUtility . getPropertyNode ( propertyName ) ; if ( propertyNode ) { const enums = IntelliSenseUtility . getEnums ( propertyNode , version ) ; for ( const value of enums ) { suggestions . push ( { isProperty : false , label : value , insertText : `"${ value }"` } ) ; } } } } param document param position provideCompletionItems ( document , position ) { if ( ! IntelliSenseUtility . enabled ( ) ) { return undefined ; } const text = DigitalTwinCompletionItemProvider . completeTextForParse ( document , position ) ; const modelContent = IntelliSenseUtility . parseDigitalTwinModel ( text ) ; if ( ! modelContent ) { return undefined ; } const node = parser . findNodeAtOffset ( modelContent . jsonNode , document . offsetAt ( position ) ) ; if ( ! node || node . type !== JsonNodeType . String ) { return undefined ; } const parent = node . parent ; if ( ! parent || parent . type !== JsonNodeType . Property || ! parent . children ) { return undefined ; } let completionItems = [ ] ; const suggestions = [ ] ; if ( node === parent . children [ 0 ] ) { const includeValue = parent . children . length < 2 ; DigitalTwinCompletionItemProvider . suggestProperty ( modelContent . version , parent , includeValue , suggestions ) ; } else { DigitalTwinCompletionItemProvider . suggestValue ( modelContent . version , parent , suggestions ) ; } const range = DigitalTwinCompletionItemProvider . evaluateOverwriteRange ( document , position , node ) ; const separator = DigitalTwinCompletionItemProvider . evaluateSeparatorAfter ( document . getText ( ) , document . offsetAt ( range . end ) ) ; completionItems = suggestions . map ( s => DigitalTwinCompletionItemProvider . createCompletionItem ( s , position , range , separator ) ) ; suggestions . length = 0 ; return completionItems ; } }