blob: a715ecd314dd55512c615c11eded67435ae780a8 [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 { getItemsOnLineCount, getItemPrefix, getItems } from './utils'
/**
* Main function to determine if there's an unclosed XML/DFDL tag at the cursor position.
* This is the core logic that drives the auto-completion behavior in closeElementSlash.ts.
* It scans the document to find the nearest tag that has been opened but not closed.
*
* @param document - The active text document
* @param position - The current cursor position
* @param nsPrefix - The XML namespace prefix (e.g., 'xs:', 'dfdl:', or empty string)
* @returns string - The name of the unclosed tag, or 'none' if all tags are closed
*/
export function checkMissingCloseTag(
document: vscode.TextDocument,
position: vscode.Position,
nsPrefix: string
) {
// Extract context about the current line
const triggerLine = position.line
const triggerPos = position.character
const triggerText = document.lineAt(triggerLine).text
const itemsOnLine = getItemsOnLineCount(triggerText)
const origPrefix = nsPrefix // Preserve original prefix for reference
// Get the list of all DFDL/XML items/tags defined in the extension
const items = getItems()
// Iterate through all possible tag types to find any unclosed instances
for (let i = 0; i < items.length; ++i) {
// Get text before cursor to analyze incomplete tags
const textBeforeTrigger = triggerText.substring(0, triggerPos)
// Adjust namespace prefix based on tag type (some tags force 'dfdl:' prefix)
nsPrefix = getItemPrefix(items[i], origPrefix)
// Find the last occurrence of this specific tag before the cursor
let tagPos = triggerText.lastIndexOf('<' + nsPrefix + items[i])
// Fallback: if not found with current prefix, try 'dfdl:' prefix
if (tagPos < 0) {
tagPos = triggerText.lastIndexOf('<dfdl:' + items[i])
if (tagPos > 0) {
nsPrefix = 'dfdl:'
}
}
// **BRANCH 1**: Multiple items on the current line
// This is a complex case requiring careful parsing to determine which tag is active
if (itemsOnLine > 1) {
// Check if this tag appears before the cursor position
if (textBeforeTrigger.lastIndexOf('<' + nsPrefix + items[i]) > -1) {
// Use specialized logic for multi-tag lines
let gt1res = getItemsForLineGT1(
triggerText,
triggerPos,
nsPrefix,
items,
i
)
// If an unclosed tag was found, return it immediately
if (gt1res != 'none') {
return gt1res
}
}
}
// **BRANCH 2**: Single item (or zero) on the current line
// This is simpler - scan upwards through the document
if (itemsOnLine < 2) {
let lt2res = getItemsForLineLT2(
document,
triggerText,
triggerLine,
nsPrefix,
items,
i
)
// If an unclosed tag was found, return it immediately
if (lt2res != 'none') {
return lt2res
}
}
}
// No unclosed tags found
return 'none'
}
/**
* Checks if the cursor is currently positioned inside a closing tag.
* Used to prevent auto-completion from interfering when manually typing closing tags.
*
* @param document - The active text document
* @param position - The current cursor position
* @returns boolean - True if cursor is inside a closing tag (between </ and >)
*/
export function cursorInsideCloseTag(
document: vscode.TextDocument,
position: vscode.Position
) {
const triggerText = document.lineAt(position.line).text
const triggerPos = position.character
// Find positions of closing tag markers
const closeTagStart = triggerText.lastIndexOf('</')
const closeTagEnd = triggerText.lastIndexOf('>')
// Cursor is inside closing tag if it's after </ but before or at >
if (
triggerPos > closeTagStart &&
triggerPos <= closeTagEnd &&
closeTagStart !== -1
) {
return true
}
return false
}
/**
* Locates the closing tag for a given tag name and position.
* Used to find where a tag is closed to understand document structure.
*
* @param document - The active text document
* @param position - The current cursor position
* @param nsPrefix - The namespace prefix for the tag
* @param tag - The tag name to find the closing tag for
* @param startLine - Starting line number for the search
* @param startPos - Starting character position for the search
* @returns [string, number, number] - Tuple: [tagName, lineNumber, position] of closing tag, or ['none', 0, 0] if not found
*/
export function getCloseTag(
document: vscode.TextDocument,
position: vscode.Position,
nsPrefix: string,
tag: string,
startLine: number,
startPos: number
): [string, number, number] {
let lineNum = startLine
let tagOpen = startPos
const triggerLine = position.line
const triggerPos = position.character
const triggerText = document.lineAt(startLine).text
const itemsOnLine = getItemsOnLineCount(document.lineAt(lineNum).text)
let endPos = triggerText.lastIndexOf('>')
// Adjust prefix based on tag type
nsPrefix = getItemPrefix(tag, nsPrefix)
// If cursor is inside a closing tag, return 'none' to avoid interfering
if (itemsOnLine === 1) {
if (cursorInsideCloseTag(document, position))
return ['none', lineNum, startPos]
}
// **CASE: Multiple items on line and cursor is inside a tag**
// Need to parse through the line to find which tag is being closed
if (itemsOnLine > 1 && startPos < endPos) {
// Iterate through tags on the line
while (tagOpen > -1 && tagOpen <= triggerPos) {
tagOpen = triggerText.indexOf('<', tagOpen)
let tagClose = triggerText.indexOf('>', tagOpen)
let tagPart = triggerText.substring(tagOpen, tagClose)
// Check if this segment is the closing tag we're looking for
if (
tagPart.includes(tag) &&
(tagPart.includes('</') || tagPart.includes('/>'))
) {
return [tag, startLine, tagOpen]
}
tagOpen = tagClose + 1
}
} else {
// **CASE: Single item per line - scan through document**
let nestedTagCount = 0 // Track nesting level for proper tag matching
let endPos = triggerText.indexOf('>', startPos)
// Special case: XML declaration tag
if (triggerText.includes('?xml version')) {
return [tag, 0, 0]
}
// If tag is already closed on this line, return it
if (
(triggerText.includes('</') || triggerText.includes('/>')) &&
triggerText.includes(tag) &&
endPos > -1 &&
itemsOnLine < 2
) {
return [tag, startLine, startPos]
}
// **MAIN LOOP**: Scan forward through document to find closing tag
while (lineNum > -1 && lineNum < document.lineCount) {
let currentText = document.lineAt(lineNum).text
let isMultiLineTag = false
// Skip comment blocks to avoid parsing tag-like content in comments
if (currentText.includes('<!--')) {
while (!currentText.includes('-->')) {
currentText = document.lineAt(++lineNum).text
}
currentText = document.lineAt(++lineNum).text
}
startPos = currentText.indexOf('<')
// Only process lines with single tags to avoid complexity
if (getItemsOnLineCount(currentText) < 2) {
// Skip lines until we find the close tag for this item
if (
currentText.includes('<' + nsPrefix + tag) &&
currentText.endsWith('>')
) {
// Scan forward to find the matching closing tag
while (!currentText.includes('</' + nsPrefix + tag)) {
currentText = document.lineAt(++lineNum).text
// Skip multi-tag lines to maintain context
if (getItemsOnLineCount(currentText) > 1) {
currentText = document.lineAt(++lineNum).text
}
// Handle nested tags of the same type
if (currentText.includes('<' + nsPrefix + tag)) {
++nestedTagCount
while (!currentText.includes('>')) {
currentText = document.lineAt(++lineNum).text
}
if (currentText.includes('/>')) {
--nestedTagCount // Self-closing tags don't affect nesting
}
}
// If we find a closing tag but it's for a nested instance, skip it
if (
currentText.includes('</' + nsPrefix + tag) &&
nestedTagCount > 0
) {
--nestedTagCount
currentText = ''
}
}
}
// **MULTI-LINE TAG HANDLING**: Tag spans multiple lines
if (
currentText.includes('<' + nsPrefix + tag) &&
!currentText.includes('>')
) {
isMultiLineTag = true
// Skip to the end of the opening tag
while (!currentText.includes('>')) {
currentText = document.lineAt(++lineNum).text
}
// If not self-closing, skip to the closing tag
if (!currentText.includes('/>')) {
while (!currentText.includes('</' + nsPrefix + tag)) {
currentText = document.lineAt(++lineNum).text
}
}
}
// **FOUND CLOSING TAG**: Return its location
if (
(currentText.includes('</' + nsPrefix + tag) &&
nestedTagCount === 0) ||
(currentText.includes('/>') && isMultiLineTag)
) {
if (isMultiLineTag) {
startPos = triggerPos
}
// If cursor is after the closing tag, return 'none'
if (
lineNum == triggerLine &&
currentText.indexOf('>', triggerPos) === -1
) {
return ['none', lineNum, startPos]
}
return [tag, lineNum, startPos]
}
}
++lineNum
}
}
return ['none', 0, 0]
}
/**
* Handles the complex case of multiple items/tags on a single line.
* Uses array tracking to determine which tags are opened vs. closed.
*
* @param triggerText - The full text of the current line
* @param triggerPos - The cursor position within the line
* @param nsPrefix - The namespace prefix
* @param items - Array of all possible tag names
* @param i - Index of the current item being checked
* @returns string - The unclosed tag name, or 'none'
*/
export function getItemsForLineGT1(
triggerText: string,
triggerPos: number,
nsPrefix: string,
items: string[],
i: number
) {
// Track positions of opening and closing tags
let openTagArray: number[] = []
let closeTagArray: number[] = []
let [nextCloseCharPos, nextOpenTagPos] = [0, 0]
// **FIND ALL OPENING TAGS**: Build array of all opening tag positions
while (
(nextOpenTagPos = triggerText.indexOf(
'<' + nsPrefix + items[i],
nextOpenTagPos
)) > -1
) {
openTagArray.push(nextOpenTagPos)
// Check if self-closing and remove from tracking if so
if ((nextCloseCharPos = triggerText.indexOf('>', nextOpenTagPos)) > -1) {
if (
triggerText.substring(nextCloseCharPos - 1, nextCloseCharPos + 1) ===
'/>'
) {
openTagArray.splice(-1, 1)
}
nextOpenTagPos = nextOpenTagPos + 1
}
}
// **FIND ALL CLOSING TAGS**: Build array of closing tag positions
while (
(nextCloseCharPos = triggerText.indexOf(
'</' + nsPrefix + items[i],
nextCloseCharPos
)) > -1
) {
closeTagArray.push(nextCloseCharPos)
nextCloseCharPos = nextCloseCharPos + 1
}
// **DETERMINE STATE**: If more opens than closes, this tag is unclosed
if (openTagArray.length > closeTagArray.length) {
return items[i]
}
return 'none'
}
/**
* Handles the simpler case of single items per line or scanning across lines.
* Looks both backwards and forwards through the document to find matching tags.
*
* @param document - The active text document
* @param triggerText - Text of the current line
* @param triggerLine - Current line number
* @param nsPrefix - Namespace prefix
* @param items - Array of all possible tag names
* @param i - Current item index
* @returns string - The unclosed tag name, or 'none'
*/
export function getItemsForLineLT2(
document: vscode.TextDocument,
triggerText: string,
triggerLine: number,
nsPrefix: string,
items: string[],
i: number
) {
// Initialize tracking variables
let [currentText, currentLine] = [triggerText, triggerLine]
let [lineBefore, lineAfter, testLine] = [
triggerLine,
triggerLine,
triggerLine,
]
let openTagArray: number[] = []
let closeTagArray: number[] = []
// Adjust prefix based on tag type
nsPrefix = getItemPrefix(items[i], nsPrefix)
// **FIND OPENING TAG**: Scan backwards until we find the opening tag
while (
currentText.indexOf('<' + nsPrefix + items[i]) === -1 &&
currentLine > -1
) {
--currentLine
if (currentLine > -1) {
currentText = document.lineAt(currentLine).text
}
// Skip multi-item lines to avoid confusion
if (getItemsOnLineCount(currentText) > 1) {
--currentLine
}
}
// If we found an opening tag, scan the document to check if it's closed
if (currentText.indexOf('<' + nsPrefix + items[i]) > -1) {
// **SCAN BACKWARDS**: Collect all opening/closing tags before current position
while (lineBefore > -1) {
currentText = document.lineAt(lineBefore).text
if (getItemsOnLineCount(currentText) < 2) {
// Found opening tag
if (currentText.indexOf('<' + nsPrefix + items[i]) > -1) {
openTagArray.push(lineBefore)
// Handle multi-line tags
let testText = currentText
if (!testText.includes('>')) {
testLine = lineBefore
while (!testText.includes('>')) {
testText = document.lineAt(++testLine).text
if (testText.indexOf('<' + nsPrefix + items[i]) > -1)
openTagArray.push(testLine)
}
}
// Remove from tracking if self-closing or already closed
if (
testText.indexOf('/>') > -1 ||
testText.includes('xml version') ||
currentText.indexOf('</' + nsPrefix + items[i]) > -1
) {
openTagArray.splice(openTagArray.length - 1, 1)
}
}
// Found closing tag
if (currentText.indexOf('</' + nsPrefix + items[i]) > -1) {
closeTagArray.push(lineBefore)
}
}
--lineBefore
}
// **SCAN FORWARDS**: Collect tags after current position
++lineAfter
while (lineAfter < document.lineCount) {
currentText = document.lineAt(lineAfter).text
if (getItemsOnLineCount(currentText) < 2) {
// Found opening tag
if (currentText.indexOf('<' + nsPrefix + items[i]) > -1) {
openTagArray.push(lineAfter)
// Handle multi-line opening tags
while (!currentText.includes('>')) {
currentText = document.lineAt(++lineAfter).text
}
// Remove if self-closing
if (currentText.indexOf('/>') > -1) {
openTagArray.splice(openTagArray.length - 1, 1)
}
}
// Found closing tag
if (currentText.indexOf('</' + nsPrefix + items[i]) > -1) {
closeTagArray.push(lineAfter)
}
}
++lineAfter
}
// **DETERMINE STATE**: More openings than closings = tag is unclosed
if (openTagArray.length > closeTagArray.length) {
return items[i]
}
}
return 'none'
}