blob: 3f1a30ea5810fac46dcd2ad6df542f767de8fee8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as vscode from 'vscode'
import { checkMissingCloseTag } from './closeUtils'
import {
insertSnippet,
checkBraceOpen,
isInXPath,
isNotTriggerChar,
getNsPrefix,
getItemPrefix,
getItemsOnLineCount,
cursorWithinBraces,
cursorWithinQuotes,
cursorAfterEquals,
} from './utils'
/**
* Creates and returns a completion item provider for the 'dfdl' language
* that triggers when the user types a forward slash '/' to close XML elements.
* This handles DFDL schema files (Data Format Description Language).
*
* @returns vscode.CompletionItemProvider - A provider that listens for '/' triggers
*/
export function getCloseElementSlashProvider() {
return vscode.languages.registerCompletionItemProvider(
'dfdl',
{
/**
* Provides completion items when '/' is typed in a DFDL document.
* Determines whether to insert a self-closing tag "/>" or a full closing tag.
*
* @param document - The active text document
* @param position - The current cursor position where '/' was typed
* @returns Promise<void> | undefined - Returns undefined or no result; edits are applied directly
*/
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
// Create position one character back from cursor to capture the trigger point
let backpos = new vscode.Position(position.line, position.character - 1)
// Get the namespace prefix (e.g., 'xs:', 'dfdl:', or empty string)
let nsPrefix = getNsPrefix(document, position)
// Get the full line text where the trigger occurred
let triggerText = document.lineAt(position.line).text
// Find the position of the last opening tag with namespace prefix
let tagPos = triggerText.lastIndexOf('<' + nsPrefix + ':')
// This line appears to be incomplete/unnecessary - likely a copy-paste error
// It searches for comma+prefix but doesn't store or use the result
triggerText.lastIndexOf(',' + nsPrefix + ':')
// If no tag with the detected namespace is found, try default dfdl namespace
if (tagPos < 0) {
tagPos = triggerText.lastIndexOf('<dfdl:')
if (tagPos > 0) {
nsPrefix = 'dfdl:'
}
}
// Re-extract text from cursor position start to current position
triggerText = document
.lineAt(position)
.text.substring(0, position.character)
// Check if there's an unclosed tag in the document up to this point
let nearestTagNotClosed = checkMissingCloseTag(
document,
position,
nsPrefix
)
// Count how many XML items exist on the current line
const itemsOnLine = getItemsOnLineCount(triggerText)
const triggerChar = '/'
// **GUARD CLAUSES**: Return early if any of these conditions are true
// These prevent auto-completion in contexts where it would be inappropriate
if (
checkBraceOpen(document, position) || // Inside unclosed curly braces {}
cursorWithinBraces(document, position) || // Cursor positioned within braces
cursorWithinQuotes(document, position) || // Inside quoted attribute values
cursorAfterEquals(document, position) || // Immediately after an equals sign
isInXPath(document, position) || // Within an XPath expression
isNotTriggerChar(document, position, triggerChar) // Verify '/' is actually the trigger char
) {
return undefined // Don't provide completion
}
// If there's a tag that needs closing, remove the '/' that was just typed
// This prevents duplicate slashes when inserting the proper closing syntax
if (!(nearestTagNotClosed == 'none')) {
let range = new vscode.Range(backpos, position)
// Perform the edit to remove the trigger character
await vscode.window.activeTextEditor?.edit((editBuilder) => {
editBuilder.replace(range, '')
})
}
// Main logic: If the line ends with '/', handle the completion
if (triggerText.endsWith('/')) {
checkItemsOnLine(
document,
position,
itemsOnLine,
nearestTagNotClosed,
backpos,
nsPrefix,
triggerText
)
}
// Return undefined as completions are handled via snippet insertion, not suggestions
//return undefined
},
},
'/' // Trigger character for this completion provider
// triggered whenever a '/' is typed
)
}
/**
* Creates and returns a completion item provider for the 'tdml' language
* that triggers when the user types a forward slash '/' to close XML elements.
* This handles TDML (Test Data Markup Language) files.
*
* @returns vscode.CompletionItemProvider - A provider that listens for '/' triggers
*/
export function getTDMLCloseElementSlashProvider() {
return vscode.languages.registerCompletionItemProvider(
'tdml',
{
/**
* Provides completion items when '/' is typed in a TDML document.
* Similar to DFDL provider but with slightly different guard conditions.
*
* @param document - The active text document
* @param position - The current cursor position where '/' was typed
* @returns undefined - Edits are applied directly, no completion list shown
*/
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
// Position one character back from cursor
let backpos = position.with(position.line, position.character - 1)
// Get namespace prefix (TDML likely uses different namespaces than DFDL)
const nsPrefix = getNsPrefix(document, position)
// Get text from line start to cursor
const triggerText = document
.lineAt(position)
.text.substring(0, position.character)
// Check for unclosed tags
let nearestTagNotClosed = checkMissingCloseTag(
document,
position,
nsPrefix
)
// Count items on current line
const itemsOnLine = getItemsOnLineCount(triggerText)
// **GUARD CLAUSES**: Similar to DFDL provider but without trigger char check
// TDML provider has slightly less restrictive conditions
if (
checkBraceOpen(document, position) || // Inside unclosed braces
cursorWithinBraces(document, position) || // Within braces region
cursorWithinQuotes(document, position) || // Inside quotes
cursorAfterEquals(document, position) || // After equals sign
isInXPath(document, position) // In XPath context
) {
return undefined // Don't provide completion
}
// Handle the slash completion if line ends with '/'
if (triggerText.endsWith('/')) {
checkItemsOnLine(
document,
position,
itemsOnLine,
nearestTagNotClosed,
backpos,
nsPrefix,
triggerText
)
}
// No completion list returned - edits applied directly
return undefined
},
},
'/' // Trigger character
// triggered whenever a '/' is typed
)
}
/**
* Determines the appropriate closing syntax to insert when '/' is typed.
* Decides between self-closing "/>" and full closing tags based on context.
*
* @param document - The active text document
* @param position - Current cursor position
* @param itemsOnLine - Number of XML items/tags on the current line
* @param nearestTagNotClosed - The nearest tag that hasn't been closed yet
* @param backpos - Position just before the '/' character
* @param nsPrefix - Namespace prefix (e.g., 'xs:', 'dfdl:')
* @param triggerText - Text from line start to cursor position
*/
function checkItemsOnLine(
document: vscode.TextDocument,
position: vscode.Position,
itemsOnLine: number,
nearestTagNotClosed: string,
backpos: vscode.Position,
nsPrefix: string,
triggerText: string
) {
// Adjust namespace prefix based on the tag type (some tags always use dfdl:)
nsPrefix = getItemPrefix(nearestTagNotClosed, nsPrefix)
// **CASE 1**: Single tag on line (or zero) AND there's a tag that needs closing
// This typically means we're at the end of a tag like <element| (cursor at |)
if (
!(nearestTagNotClosed == 'none') &&
(itemsOnLine == 1 || itemsOnLine == 0)
) {
// Special handling for variable-related tags: add newline after self-closing
// This follows DFDL best practices for readability
if (
nearestTagNotClosed.includes('defineVariable') ||
nearestTagNotClosed.includes('setVariable')
) {
insertSnippet('/>\n', backpos)
} else {
// Standard self-closing tag with cursor positioned after
insertSnippet('/>$0', backpos)
}
}
// **CASE 2**: Multiple items on the same line
// This is more complex - need to determine if we're inside a tag or need full close
if (itemsOnLine > 1) {
if (
triggerText.endsWith('/') &&
triggerText.includes('<' + nsPrefix + nearestTagNotClosed)
) {
// Find where this specific tag starts on the line
let tagPos = triggerText.lastIndexOf('<' + nsPrefix + nearestTagNotClosed)
let tagEndPos = triggerText.indexOf('>', tagPos)
// **CONDITIONS FOR FULL CLOSING TAG**:
// 1. Tag exists on line, AND
// 2. Tag is NOT already self-closing (no "/>"), AND
// 3. Cursor is positioned AFTER the opening ">", AND
// 4. No closing tag exists yet for this element
if (
tagPos != -1 &&
!triggerText.substring(tagEndPos - 1, 2).includes('/>') &&
triggerText
.substring(backpos.character - 1, backpos.character)
.includes('>') &&
!triggerText
.substring(tagEndPos)
.includes('</' + nsPrefix + nearestTagNotClosed)
) {
// Insert full closing tag (e.g., </xs:element>)
insertSnippet('</' + nsPrefix + nearestTagNotClosed + '>$0', backpos)
} else {
// Default to self-closing tag
insertSnippet('/>$0', backpos)
}
}
}
}