import { map , findKey , find , forEachRight , includes } from "lodash" ; import { CompletionItem , CompletionItemKind , TextDocumentPositionParams , InsertTextFormat , TextEdit , Range , } from "vscode-languageserver" ; import { TextDocument } from "vscode-languageserver-textdocument" ; import { Position , MarkupContent } from "vscode-languageserver-types" ; import { parse , DocumentCstNode } from "@xml-tools/parser" ; import { buildAst , DEFAULT_NS , SourcePosition , XMLAttribute , XMLElement , XMLDocument , } from "@xml-tools/ast" ; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types" ; import { getXMLViewCompletions , UI5XMLViewCompletion , UI5ClassesInXMLTagNameCompletion , isUI5NodeXMLViewCompletion , } from "@ui5-language-assistant/xml-views-completion" ; import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils" ; import { getNodeDocumentation , getNodeDetail } from "./documentation" ; import { Settings } from "@ui5-language-assistant/settings" ; import { assertNever } from "assert-never" ; export function getCompletionItems ( opts ) { const documentText = opts . document . getText ( ) ; const { cst , tokenVector } = parse ( documentText ) ; const ast = buildAst ( cst as DocumentCstNode , tokenVector ) ; const suggestions = getXMLViewCompletions ( { model : opts . model , offset : opts . document . offsetAt ( opts . textDocumentPosition . position ) , cst : cst as DocumentCstNode , ast : ast , tokenVector : tokenVector , settings : { codeAssist : opts . documentSettings . codeAssist } , } ) ; const completionItems = transformToLspSuggestions ( suggestions , opts . model , opts . textDocumentPosition ) ; return completionItems ; } function transformToLspSuggestions ( suggestions , model , textDocumentPosition ) { const lspSuggestions = map ( suggestions , ( suggestion ) => { const lspKind = computeLSPKind ( suggestion ) ; const textEditDetails = createTextEdits ( suggestion , textDocumentPosition . position ) ; const documentation = getDocumentation ( suggestion , model ) ; const completionItem = { label : getLabel ( suggestion ) , filterText : textEditDetails . filterText , textEdit : textEditDetails . textEdit , insertTextFormat : InsertTextFormat . Snippet , additionalTextEdits : textEditDetails . additionalTextEdits , detail : getDetail ( suggestion ) , documentation : documentation , kind : lspKind , } ; return completionItem ; } ) ; return lspSuggestions ; } export function computeLSPKind ( suggestion ) { switch ( suggestion . type ) { case "UI5NamespacesInXMLAttributeKey" : case "UI5NamespacesInXMLAttributeValue" : return CompletionItemKind . Module ; case "UI5AssociationsInXMLAttributeKey" : return CompletionItemKind . Reference ; case "UI5AggregationsInXMLTagName" : return CompletionItemKind . Field ; case "UI5PropsInXMLAttributeKey" : return CompletionItemKind . Property ; case "UI5ClassesInXMLTagName" : return CompletionItemKind . Class ; case "UI5EventsInXMLAttributeKey" : return CompletionItemKind . Event ; case "UI5EnumsInXMLAttributeValue" : return CompletionItemKind . EnumMember ; case "BooleanValueInXMLAttributeValue" : return CompletionItemKind . Constant ; default : assertNever ( suggestion , true ) ; return CompletionItemKind . Text ; } } function createTextEdits ( suggestion , originalPosition ) { const additionalTextEdits = [ ] ; let range = { start : originalPosition , end : originalPosition , } ; let newText = suggestion . ui5Node . name ; let filterText = newText ; switch ( suggestion . type ) { case "UI5NamespacesInXMLAttributeKey" : { range = getXMLAttributeKeyRange ( suggestion . astNode ) ?? range ; newText = `xmlns:${ newText }` ; filterText = newText ; if ( suggestion . astNode . syntax . value === undefined ) { newText += `="${ ui5NodeToFQN ( suggestion . ui5Node ) }"` ; } break ; } case "UI5AssociationsInXMLAttributeKey" : case "UI5EventsInXMLAttributeKey" : { range = getXMLAttributeKeyRange ( suggestion . astNode ) ?? range ; if ( suggestion . astNode . syntax . value === undefined ) { newText += '="${0}"' ; } break ; } case "UI5PropsInXMLAttributeKey" : { range = getXMLAttributeKeyRange ( suggestion . astNode ) ?? range ; if ( suggestion . astNode . syntax . value === undefined ) { const defaultValue = suggestion . ui5Node . default ; if ( defaultValue !== undefined && includes ( [ "number" , "boolean" , "string" ] , typeof defaultValue ) ) { newText += `="\${0:${ defaultValue }}"` ; } else { newText += '="${0}"' ; } } break ; } case "UI5AggregationsInXMLTagName" : { range = getXMLTagNameRange ( suggestion . astNode ) ?? range ; let parentNS = undefined ; if ( suggestion . astNode . parent . type === "XMLElement" ) { parentNS = suggestion . astNode . parent . ns ; if ( parentNS !== undefined && parentNS !== "" ) { parentNS += ":" ; } } const tagName = `${ parentNS ?? "" }${ suggestion . ui5Node . name }` ; filterText = `${ parentNS ?? "" }${ suggestion . ui5Node . name }` ; if ( shouldCloseXMLElement ( suggestion . astNode ) ) { newText = `${ tagName }>\${0}</${ tagName }>` ; } else { additionalTextEdits . push ( ... getClosingTagTextEdits ( suggestion . astNode , tagName ) ) ; } break ; } case "UI5ClassesInXMLTagName" : { ( { range , newText , filterText } = createTextEditsForClassInTagName ( range , suggestion , additionalTextEdits ) ) ; break ; } case "UI5NamespacesInXMLAttributeValue" : { range = getXMLAttributeValueRange ( suggestion . astNode ) ?? range ; newText = `"${ ui5NodeToFQN ( suggestion . ui5Node ) }"` ; filterText = newText ; break ; } case "UI5EnumsInXMLAttributeValue" : { range = getXMLAttributeValueRange ( suggestion . astNode ) ?? range ; newText = `"${ newText }"` ; filterText = newText ; break ; } case "BooleanValueInXMLAttributeValue" : { range = getXMLAttributeValueRange ( suggestion . astNode ) ?? range ; newText = `"${ suggestion . ui5Node . value }"` ; filterText = newText ; break ; } } return { textEdit : { range , newText , } , filterText , additionalTextEdits , } ; } function createTextEditsForClassInTagName ( originalRange , suggestion , additionalTextEdits ) { const range = getXMLTagNameRange ( suggestion . astNode ) ?? originalRange ; let newText = suggestion . ui5Node . name ; let tagName = newText ; const nsPrefix = getClassNamespacePrefix ( suggestion , additionalTextEdits ) ; if ( nsPrefix !== undefined ) { tagName = `${ nsPrefix }:${ tagName }` ; newText = tagName ; } forEachRight ( additionalTextEdits , ( edit , index ) => { if ( rangeContains ( range , edit . range ) ) { newText += edit . newText ; additionalTextEdits . splice ( index , 1 ) ; } } ) ; if ( shouldCloseXMLElement ( suggestion . astNode ) ) { newText += ` \${1}>\${0}</${ tagName }>` ; } else { additionalTextEdits . push ( ... getClosingTagTextEdits ( suggestion . astNode , tagName ) ) ; } const filterText = `${ tagName } ${ ui5NodeToFQN ( suggestion . ui5Node ) }` ; return { range , newText , filterText } ; } function getClosingTagTextEdits ( xmlElement , closingTagName ) { const textEdits = [ ] ; if ( xmlElement . syntax . closeName !== undefined && xmlElement . syntax . closeName . image === xmlElement . syntax . openName ?. image ) { const closingTagNameRange = positionToRange ( xmlElement . syntax . closeName ) ; if ( closingTagNameRange !== undefined ) { const closingTagNameTextEdit = { newText : closingTagName , range : closingTagNameRange , } ; textEdits . push ( closingTagNameTextEdit ) ; } } else if ( xmlElement . syntax . closeName === undefined && xmlElement . syntax . closeBody !== undefined ) { const closingTagRange = positionToRange ( xmlElement . syntax . closeBody ) ; if ( closingTagRange !== undefined ) { const closingTagTextEdit = { newText : `</${ closingTagName }>` , range : closingTagRange , } ; textEdits . push ( closingTagTextEdit ) ; } } return textEdits ; } function shouldCloseXMLElement ( xmlElement ) { return ( xmlElement . syntax . closeBody === undefined && ( xmlElement . syntax . openName === undefined || xmlElement . syntax . openName . endOffset === xmlElement . position . endOffset ) ) ; } function getXMLTagNameRange ( xmlElement ) { return positionToRange ( xmlElement . syntax . openName ) ; } function getXMLAttributeKeyRange ( xmlAttribute ) { return positionToRange ( xmlAttribute . syntax . key ) ; } function getXMLAttributeValueRange ( xmlAttribute ) { return positionToRange ( xmlAttribute . syntax . value ) ; } function rangeContains ( range , inner ) { function atMost ( position1 , position2 ) { return ( position1 . line < position2 . line || ( position1 . line === position2 . line && position1 . character <= position2 . character ) ) ; } return atMost ( range . start , inner . start ) && atMost ( inner . end , range . end ) ; } /**
 * Create an insert range at the position right after the sent column.
 * For exmaple, for text "12345" the character in position 1 is "1" and the
 * insert position after it is "1|2345".
 * The character in position 2 is "2" and the insert position after it is "12|345".
 * The character at position 5 is "5" and the insert position after it at the end of the text -> "12345|".
 * */ function createInsertRangeAfter ( line , column ) { return { start : Position . create ( line - 1 , column ) , end : Position . create ( line - 1 , column ) , } ; } /**
 * Create an insert range at the position right before the sent column.
 * For exmaple, for text "12345" the character in position 1 is "1" and the
 * insert position before it is the beginning of the text -> "|12345".
 * The character in position 2 is "2" and the insert position before it is "1|2345".
 * The character at position 5 is "5" and the insert position before it is "1234|5".
 * */ function createInsertRangeBefore ( line , column ) { return { start : Position . create ( line - 1 , column - 1 ) , end : Position . create ( line - 1 , column - 1 ) , } ; } function positionToRange ( position ) { function isDummyPosition ( position ) { return position . startLine < 0 || position . endLine < 0 ; } if ( position !== undefined && ! isDummyPosition ( position ) ) { return { start : Position . create ( position . startLine - 1 , position . startColumn - 1 ) , end : Position . create ( position . endLine - 1 , position . endColumn ) , } ; } return undefined ; } function getClassNamespacePrefix ( suggestion , additionalTextEdits ) { const xmlElement = suggestion . astNode ; const parent = suggestion . ui5Node . parent ; if ( parent !== undefined ) { const parentFQN = ui5NodeToFQN ( parent ) ; let xmlnsPrefix = findKey ( xmlElement . namespaces , ( _ ) => _ === parentFQN ) ; if ( xmlnsPrefix === undefined ) { xmlnsPrefix = parent . name ; let i = 2 ; while ( find ( xmlElement . namespaces , ( v , k ) => k === xmlnsPrefix ) !== undefined ) { xmlnsPrefix = parent . name + i ; ++ i ; } const addNamespaceEdit = getAddNamespaceEdit ( xmlElement , xmlnsPrefix , parentFQN ) ; if ( addNamespaceEdit !== undefined ) { additionalTextEdits . push ( addNamespaceEdit ) ; } } if ( xmlnsPrefix !== undefined && xmlnsPrefix !== DEFAULT_NS && xmlnsPrefix . length > 0 ) { return xmlnsPrefix ; } } return undefined ; } function getAddNamespaceEdit ( xmlElement , xmlns , value ) { let parent = xmlElement ; while ( parent . type !== "XMLDocument" ) { parent = parent . parent ; } if ( parent . rootElement !== null ) { let range ; if ( parent . rootElement . syntax . openName !== undefined ) { const position = parent . rootElement . syntax . openName ; range = createInsertRangeAfter ( position . endLine , position . endColumn ) ; } else { const position = parent . rootElement . syntax . openBody ?? parent . rootElement . position ; range = createInsertRangeBefore ( position . startLine , position . startColumn + "<" . length ) ; } return { range , newText : ` xmlns:${ xmlns }="${ value }"` , } ; } return undefined ; } function getLabel ( suggestion ) { return suggestion . ui5Node . name ; } function getDocumentation ( suggestion , model ) { if ( ! isUI5NodeXMLViewCompletion ( suggestion ) ) { return undefined ; } return getNodeDocumentation ( suggestion . ui5Node , model ) ; } function getDetail ( suggestion ) { if ( ! isUI5NodeXMLViewCompletion ( suggestion ) ) { return undefined ; } let detailText = getNodeDetail ( suggestion . ui5Node ) ; if ( suggestion . ui5Node . experimentalInfo ?. isExperimental ) { detailText = `(experimental) ${ detailText }` ; } if ( suggestion . ui5Node . deprecatedInfo ?. isDeprecated ) { detailText = `(deprecated) ${ detailText }` ; } return detailText ; }