This is an automated email from the ASF dual-hosted git repository.
jeremyyao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil-vscode.git
The following commit(s) were added to refs/heads/main by this push:
new 3ec9f1b Document IntelliSense functionallity for
CloseElementSlashProvider in closeElementSlashProvider.ts and closeUtils.ts.
3ec9f1b is described below
commit 3ec9f1baa13a93213cb4df8bea1a8d0506ca7d18
Author: Jeremy Yao <[email protected]>
AuthorDate: Mon Jan 19 03:33:28 2026 -0500
Document IntelliSense functionallity for CloseElementSlashProvider in
closeElementSlashProvider.ts and closeUtils.ts.
Closes #1492
---
src/language/intellisense-development.md | 115 ++++++++++++++++++++++
src/language/providers/closeElementSlash.ts | 142 ++++++++++++++++++++++------
src/language/providers/closeUtils.ts | 138 +++++++++++++++++++++++----
3 files changed, 348 insertions(+), 47 deletions(-)
diff --git a/src/language/intellisense-development.md
b/src/language/intellisense-development.md
index 730317d..6ca382c 100644
--- a/src/language/intellisense-development.md
+++ b/src/language/intellisense-development.md
@@ -41,6 +41,8 @@ This document contains an overview of how Intellisense works,
as well as a gener
- [attributeCompletion.ts](#attributecompletionts)
- [attributeHover.ts](#attributehoverts)
- [attributeValueCompletion.ts](#attributevaluecompletionts)
+ - [closeElementSlash.ts](#closeelementslashts)
+ - [closeUtils.ts](#closeutilsts)
- [Intellisense Data Files (intellisense
subdirectory)](#intellisense-data-files-intellisense-subdirectory)
- [attributeItems.ts](#attributeitemsts)
- [attributeValueItems.ts](#attributevalueitemsts)
@@ -196,6 +198,119 @@ Hover tooltips can be found under
`attributeHoverValues()` in `attributeHoverIte
**Trigger:** Space (` `)
+###### closeElementSlash.ts
+
+**Purpose:** Completion provider that handles auto-closing XML/DFDL elements
when the user types a forward slash '/' character, determining whether to
insert self-closing tags (`/>`) or full closing tags (`</tag>`).
+
+**Key Functionality:**
+
+- Provides intelligent slash-based auto-completion for both DFDL and TDML files
+- Determines whether to insert self-closing (`/>$0`) or full closing tags
(`</ns:tag>$0`) based on context
+- Prevents inappropriate completion inside XPath expressions, quoted strings,
braces, or after equals signs
+- Handles complex multi-tag lines by analyzing tag positions and existing
closing tags
+- Special case handling for variable-related tags (`defineVariable`,
`setVariable`) with added newlines for readability
+- Direct document manipulation using snippet insertion rather than traditional
completion suggestions
+
+**Key Functions:**
+
+- `getCloseElementSlashProvider()`: Main DFDL completion provider registration
+- `getTDMLCloseElementSlashProvider()`: TDML-specific completion provider
registration
+- `checkItemsOnLine()`: Core logic determining what to insert based on tag
count, namespace, and context
+
+**Trigger:** Forward slash (`/`)
+
+**Architecture Notes:**
+
+- Uses **guard clauses** for early returns in inappropriate contexts
+- Implements **dual strategy**: simpler TDML provider vs. more robust DFDL
provider
+- Follows VS Code provider pattern but uses **direct edits** (`insertSnippet`)
instead of completion lists
+- Separation of concerns: context validation in providers, insertion logic in
`checkItemsOnLine()`
+
+**Dependencies:**
+
+- `closeUtils.checkMissingCloseTag()`: Determines if a tag needs closing
+- `utils.insertSnippet()`: Performs the actual text insertion
+- Multiple context validators: `checkBraceOpen()`, `cursorWithinBraces()`,
`cursorWithinQuotes()`, `cursorAfterEquals()`, `isInXPath()`,
`isNotTriggerChar()`
+- Namespace utilities: `getNsPrefix()`, `getItemPrefix()`,
`getItemsOnLineCount()`
+
+**Flow:**
+
+1. User types '/' at cursor position
+2. Provider validates context (not in XPath, quotes, braces, etc.)
+3. `checkMissingCloseTag()` scans document to find nearest unclosed tag
+4. If tag needs closing, removes the trigger '/' to prevent duplicates
+5. `checkItemsOnLine()` decides: self-closing tag, full closing tag, or nothing
+6. Inserts appropriate snippet at the correct position
+
+###### closeUtils.ts
+
+**Purpose:** Core utility module providing tag state analysis functions for
the auto-completion system. Determines whether XML/DFDL tags are properly
closed and identifies unclosed tags that need completion.
+
+**Key Functionality:**
+
+- **Tag State Analysis**: `checkMissingCloseTag()` is the primary function
that scans documents to find opened-but-unclosed tags
+- **Dual Strategy Implementation**: Uses different algorithms for single-tag
lines vs. multi-tag lines:
+ - `getItemsForLineGT1()`: Array-based position tracking for complex
multi-tag scenarios
+ - `getItemsForLineLT2()`: Bidirectional document scanning for simpler
single-tag situations
+- **Nested Tag Awareness**: Properly tracks nesting levels to handle complex
XML structures with same-named nested elements
+- **Multi-line Tag Support**: Extensive logic for tags spanning multiple lines
(common in DFDL with many attributes)
+- **Namespace Prefix Management**: Dynamically adjusts namespace prefixes
based on tag types and document context
+- **Closing Tag Location**: `getCloseTag()` finds exact positions of closing
tags for document structure analysis
+- **Cursor Context Validation**: `cursorInsideCloseTag()` prevents
interference when user is manually typing closing tags
+
+**Key Functions:**
+
+- `checkMissingCloseTag()`: Main entry point - returns name of unclosed tag or
'none'
+- `checkItemsForLineGT1()`: Multi-tag line analysis using opening/closing
position arrays
+- `checkItemsForLineLT2()`: Single-tag line analysis with forward/backward
scanning
+- `getCloseTag()`: Locates closing tag positions with nested tag tracking
+- `cursorInsideCloseTag()`: Checks if cursor is positioned inside a closing tag
+- `getItemsForLineGT1()`: Complex multi-tag line parser
+- `getItemsForLineLT2()`: Simple single-tag line parser
+
+**Algorithm Details:**
+
+**`getItemsForLineGT1()` (Multi-tag lines):**
+
+1. Builds arrays of all opening and closing tag positions on the line
+2. Filters out self-closing tags from the opening array
+3. Compares array lengths - more opens than closes = unclosed tag
+4. Efficient for complex scenarios but requires careful position tracking
+
+**`getItemsForLineLT2()` (Single-tag lines):**
+
+1. Scans backwards to find the opening tag
+2. Scans both directions (backwards and forwards) to collect all
opening/closing tags
+3. Handles multi-line tags by traversing until finding closing `>`
+4. Removes self-closing tags from tracking
+5. Compares open/close counts across the entire document context
+
+**`getCloseTag()` (Closing tag locator):**
+
+1. Tracks `nestedTagCount` for proper nested element handling
+2. Skips comment blocks (`<!-- -->`) to avoid false positives
+3. Handles multi-line tags by aggregating text across lines
+4. Returns precise line and character position of closing tags
+
+**Dependencies:**
+
+- `utils.getItemsOnLineCount()`: Counts XML items on a line
+- `utils.getItemPrefix()`: Determines correct namespace prefix for tags
+- `utils.getItems()`: Gets list of all DFDL/XSD tag names
+
+**Integration:**
+
+- Called exclusively by `closeElementSlash.ts` providers
+- Provides the "brains" of the auto-completion system
+- Returns tag names that drive snippet insertion decisions
+
+**Performance Considerations:**
+
+- Early returns when tag is found
+- Avoids full document parsing by scanning only relevant sections
+- Skips complex multi-tag lines in single-tag mode
+- Uses efficient string search methods (`indexOf`, `lastIndexOf`)
+
##### Intellisense Data Files (intellisense subdirectory)
###### attributeItems.ts
diff --git a/src/language/providers/closeElementSlash.ts
b/src/language/providers/closeElementSlash.ts
index c2fff16..3f1a30e 100644
--- a/src/language/providers/closeElementSlash.ts
+++ b/src/language/providers/closeElementSlash.ts
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import * as vscode from 'vscode'
import { checkMissingCloseTag } from './closeUtils'
import {
@@ -29,56 +28,91 @@ import {
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) ||
- cursorWithinBraces(document, position) ||
- cursorWithinQuotes(document, position) ||
- cursorAfterEquals(document, position) ||
- isInXPath(document, position) ||
- isNotTriggerChar(document, position, triggerChar)
+ 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
+ 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,
@@ -90,45 +124,69 @@ export function getCloseElementSlashProvider() {
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
)
- const itemsOnLine = getItemsOnLineCount(triggerText)
+ // 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) ||
- cursorWithinBraces(document, position) ||
- cursorWithinQuotes(document, position) ||
- cursorAfterEquals(document, position) ||
- isInXPath(document, position)
+ 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
+ return undefined // Don't provide completion
}
-
+ // Handle the slash completion if line ends with '/'
if (triggerText.endsWith('/')) {
checkItemsOnLine(
document,
@@ -140,15 +198,26 @@ export function getTDMLCloseElementSlashProvider() {
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,
@@ -158,30 +227,41 @@ function checkItemsOnLine(
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('/>') &&
@@ -192,8 +272,10 @@ function checkItemsOnLine(
.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)
}
}
diff --git a/src/language/providers/closeUtils.ts
b/src/language/providers/closeUtils.ts
index 297e1c3..a715ecd 100644
--- a/src/language/providers/closeUtils.ts
+++ b/src/language/providers/closeUtils.ts
@@ -18,24 +18,43 @@
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
+ 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) {
@@ -43,8 +62,12 @@ export function checkMissingCloseTag(
}
}
+ // **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,
@@ -53,12 +76,15 @@ export function checkMissingCloseTag(
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,
@@ -69,24 +95,37 @@ export function checkMissingCloseTag(
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 &&
@@ -97,6 +136,18 @@ export function cursorInsideCloseTag(
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,
@@ -112,19 +163,26 @@ export function getCloseTag(
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('/>'))
@@ -135,13 +193,16 @@ export function getCloseTag(
tagOpen = tagClose + 1
}
} else {
- let nestedTagCount = 0
+ // **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) &&
@@ -151,11 +212,12 @@ export function getCloseTag(
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 any comment lines
+ // Skip comment blocks to avoid parsing tag-like content in comments
if (currentText.includes('<!--')) {
while (!currentText.includes('-->')) {
currentText = document.lineAt(++lineNum).text
@@ -165,32 +227,34 @@ export function getCloseTag(
startPos = currentText.indexOf('<')
+ // Only process lines with single tags to avoid complexity
if (getItemsOnLineCount(currentText) < 2) {
- //skip lines until the close tag for this item
+ // Skip lines until we find the close tag for this item
if (
currentText.includes('<' + nsPrefix + tag) &&
currentText.endsWith('>')
) {
- //skipping to closing tag
+ // Scan forward to find the matching closing tag
while (!currentText.includes('</' + nsPrefix + tag)) {
currentText = document.lineAt(++lineNum).text
- //If currentText is multi tag line skip to next line
+ // 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
+ --nestedTagCount // Self-closing tags don't affect nesting
}
}
- //if currentText is a closing tag
+ // If we find a closing tag but it's for a nested instance, skip it
if (
currentText.includes('</' + nsPrefix + tag) &&
nestedTagCount > 0
@@ -201,19 +265,19 @@ export function getCloseTag(
}
}
- //if end tag symbol is on a different line
+ // **MULTI-LINE TAG HANDLING**: Tag spans multiple lines
if (
currentText.includes('<' + nsPrefix + tag) &&
!currentText.includes('>')
) {
isMultiLineTag = true
- //skip to the end tag symbol
+ // Skip to the end of the opening tag
while (!currentText.includes('>')) {
currentText = document.lineAt(++lineNum).text
}
- //if the tag isn't self closing, skip to the closing tag
+ // If not self-closing, skip to the closing tag
if (!currentText.includes('/>')) {
while (!currentText.includes('</' + nsPrefix + tag)) {
currentText = document.lineAt(++lineNum).text
@@ -221,6 +285,7 @@ export function getCloseTag(
}
}
+ // **FOUND CLOSING TAG**: Return its location
if (
(currentText.includes('</' + nsPrefix + tag) &&
nestedTagCount === 0) ||
@@ -230,7 +295,7 @@ export function getCloseTag(
startPos = triggerPos
}
- //if the cursor is after the closing tag
+ // If cursor is after the closing tag, return 'none'
if (
lineNum == triggerLine &&
currentText.indexOf('>', triggerPos) === -1
@@ -247,6 +312,17 @@ export function getCloseTag(
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,
@@ -254,10 +330,12 @@ export function getItemsForLineGT1(
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],
@@ -266,8 +344,8 @@ export function getItemsForLineGT1(
) {
openTagArray.push(nextOpenTagPos)
+ // Check if self-closing and remove from tracking if so
if ((nextCloseCharPos = triggerText.indexOf('>', nextOpenTagPos)) > -1) {
- //if tag is self closing remove it from the openTagArray
if (
triggerText.substring(nextCloseCharPos - 1, nextCloseCharPos + 1) ===
'/>'
@@ -279,6 +357,7 @@ export function getItemsForLineGT1(
}
}
+ // **FIND ALL CLOSING TAGS**: Build array of closing tag positions
while (
(nextCloseCharPos = triggerText.indexOf(
'</' + nsPrefix + items[i],
@@ -289,6 +368,7 @@ export function getItemsForLineGT1(
nextCloseCharPos = nextCloseCharPos + 1
}
+ // **DETERMINE STATE**: If more opens than closes, this tag is unclosed
if (openTagArray.length > closeTagArray.length) {
return items[i]
}
@@ -296,6 +376,18 @@ export function getItemsForLineGT1(
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,
@@ -304,6 +396,7 @@ export function getItemsForLineLT2(
items: string[],
i: number
) {
+ // Initialize tracking variables
let [currentText, currentLine] = [triggerText, triggerLine]
let [lineBefore, lineAfter, testLine] = [
triggerLine,
@@ -313,8 +406,10 @@ export function getItemsForLineLT2(
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
@@ -325,20 +420,24 @@ export function getItemsForLineLT2(
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)
- //if multi line tag
+ // Handle multi-line tags
let testText = currentText
if (!testText.includes('>')) {
testLine = lineBefore
@@ -349,7 +448,7 @@ export function getItemsForLineLT2(
}
}
- //if selfclosing remove from the array
+ // Remove from tracking if self-closing or already closed
if (
testText.indexOf('/>') > -1 ||
testText.includes('xml version') ||
@@ -359,6 +458,7 @@ export function getItemsForLineLT2(
}
}
+ // Found closing tag
if (currentText.indexOf('</' + nsPrefix + items[i]) > -1) {
closeTagArray.push(lineBefore)
}
@@ -367,26 +467,29 @@ export function getItemsForLineLT2(
--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)
- //if multi line tag
+ // Handle multi-line opening tags
while (!currentText.includes('>')) {
currentText = document.lineAt(++lineAfter).text
}
- //if selfclosing remove from the array
+ // 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)
}
@@ -395,6 +498,7 @@ export function getItemsForLineLT2(
++lineAfter
}
+ // **DETERMINE STATE**: More openings than closings = tag is unclosed
if (openTagArray.length > closeTagArray.length) {
return items[i]
}