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 eed1c54 Document IntelliSense CloseElementProvider
eed1c54 is described below
commit eed1c549324647de53c4c765dceb0e073669cb1c
Author: Jeremy Yao <[email protected]>
AuthorDate: Tue Jan 20 19:50:42 2026 -0500
Document IntelliSense CloseElementProvider
Closes #1491
---
src/language/intellisense-development.md | 72 ++++++++++++++
src/language/providers/closeElement.ts | 155 +++++++++++++++++++++++++++++--
2 files changed, 217 insertions(+), 10 deletions(-)
diff --git a/src/language/intellisense-development.md
b/src/language/intellisense-development.md
index 6ca382c..a4128c7 100644
--- a/src/language/intellisense-development.md
+++ b/src/language/intellisense-development.md
@@ -41,6 +41,7 @@ This document contains an overview of how Intellisense works,
as well as a gener
- [attributeCompletion.ts](#attributecompletionts)
- [attributeHover.ts](#attributehoverts)
- [attributeValueCompletion.ts](#attributevaluecompletionts)
+ - [closeElement.ts](#closeelementts)
- [closeElementSlash.ts](#closeelementslashts)
- [closeUtils.ts](#closeutilsts)
- [Intellisense Data Files (intellisense
subdirectory)](#intellisense-data-files-intellisense-subdirectory)
@@ -198,6 +199,77 @@ Hover tooltips can be found under `attributeHoverValues()`
in `attributeHoverIte
**Trigger:** Space (` `)
+###### closeElement.ts
+
+**Purpose:** Completion provider that handles auto-completion of XML element
structures when the user types a `>` character. Unlike `closeElementSlash.ts`
which handles closing tags with `/`, this provider completes the element
opening and provides the corresponding closing tag structure.
+
+**Key Functionality:**
+
+- Provides intelligent element completion for both DFDL and TDML files when
`>` is typed
+- Determines whether to insert simple closing tags or full container
structures based on context
+- Prevents inappropriate completion inside XPath expressions, quoted strings,
braces, or after equals signs
+- **Special trigger patterns:**
+ - `>`: Standard trigger for multi-line completion with indentation
+ - `>>`: Inline completion without extra whitespace
+ - `.=>`: Alternative trigger pattern for special cases
+- **Tag-specific formatting:**
+ - `schema` tag: Gets unique multi-line formatting with proper XML document
structure
+ - Variable tags (`defineVariable`, `setVariable`): Multi-line snippets with
newlines for readability
+ - Assertion tags (`assert`, `discriminator`): Inline snippets with tab stops
for test expressions
+ - Default: Multi-line container format with indentation for child elements
+- Handles complex multi-tag lines by analyzing tag positions and existing
closing tags
+- **Progressive completion workflow:** Supports tab-stop navigation ($0, $1)
for seamless content insertion
+- Direct document manipulation using snippet insertion rather than traditional
completion suggestions
+
+**Key Functions:**
+
+- `getCloseElementProvider()`: Main DFDL completion provider registration
+- `getTDMLCloseElementProvider()`: TDML-specific completion provider
registration
+- `checkItemsOnLine()`: Core logic determining what to insert based on item
count and context
+- `checkNearestTagNotClosed()`: Handles tag-specific snippet patterns for
single-item lines
+- `checkTriggerText()`: Manages insertion for complex multi-tag lines
+
+**Trigger:** Greater-than sign (`>`)
+
+**Architecture Notes:**
+
+- Uses **guard clauses** for early returns in inappropriate contexts (same
pattern as `closeElementSlash.ts`)
+- Implements **dual strategy**: DFDL provider with full validation vs. simpler
TDML provider
+- Follows VS Code provider pattern but uses **direct edits** (`insertSnippet`)
instead of completion lists
+- **Snippet complexity:** Uses multi-tab-stop patterns ($1 for content, $0 for
final position) enabling progressive workflow
+- Separation of concerns: Context validation in providers, insertion logic
delegated to helper functions
+
+**Dependencies:**
+
+- `closeUtils.checkMissingCloseTag()`: Determines which tag needs closing
+- `utils.insertSnippet()`: Performs the actual text insertion with tab stops
+- 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.) and
verifies `>` is the trigger char
+3. `checkMissingCloseTag()` scans document to find the nearest unclosed tag
+4. If tag needs closing, checks trigger pattern (`>`, `>>`, or `.=>`) and
items on line
+5. Deletes the typed trigger character(s) to prevent duplication
+6. `checkItemsOnLine()` delegates to specialized handlers:
+ - `checkNearestTagNotClosed()` for single-item lines (tag-specific
formatting)
+ - `checkTriggerText()` for multi-tag lines (ensures no duplicate closing
tags)
+7. Inserts appropriate snippet with proper formatting and tab stops
+
+**Comparison with `closeElementSlash.ts`:**
+
+| Feature | `closeElement.ts` (This File) |
`closeElementSlash.ts` |
+| ---------------------- | ------------------------------------------------ |
--------------------------------------------- |
+| **Trigger** | `>` character |
`/` character |
+| **Primary Action** | Completes element opening with closing structure |
Closes element (self-closing or full closing) |
+| **Snippet Complexity** | Multi-tab-stop ($0, $1) for progressive workflow |
Single tab-stop ($0) only |
+| **Special Patterns** | `>`, `>>`, `.=>` triggers |
`/` only |
+| **Schema Handling** | Special multi-line formatting with indentation |
Standard completion |
+| **Content Insertion** | Yes (between opening and closing tags) |
No (cursor after closing) |
+| **Use Case** | Creating new elements with content |
Closing existing elements |
+
###### 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>`).
diff --git a/src/language/providers/closeElement.ts
b/src/language/providers/closeElement.ts
index c78ef8a..bc01c17 100644
--- a/src/language/providers/closeElement.ts
+++ b/src/language/providers/closeElement.ts
@@ -32,69 +32,131 @@ import {
getItemPrefix,
} from './utils'
+/**
+ * Registers a completion provider for the 'dfdl' language that triggers when
the user types a '>' character.
+ * This provider auto-completes element closing tags and structures,
differentiating from closeElementSlash.ts which triggers on '/'.
+ *
+ * **Trigger Behavior:** When '>' is typed, it replaces it with:
+ * - `></tag>` for simple elements
+ * - `>\n\t$0\n</tag>` for container elements with proper indentation
+ * - Special handling for multi-tag lines and schema elements
+ *
+ */
export function getCloseElementProvider() {
return vscode.languages.registerCompletionItemProvider(
'dfdl',
{
+ /**
+ * Provides completion items when '>' is typed in a DFDL document.
+ * Implements context-aware snippet insertion with support for
progressive completion.
+ *
+ * **Guard Clauses:** Returns early if cursor is in XPath, quotes,
braces, after equals, or '>' isn't the trigger char.
+ * This prevents unwanted completions in inappropriate contexts.
+ *
+ * **Snippet Strategy:** Uses tab-stop patterns:
+ * - `$0` = Final cursor position
+ * - `$1` = First tab stop (between opening/closing tags for content)
+ * - `\n\t$0\n` = Newline with indentation for container elements
+ *
+ * **Special Patterns:**
+ * - `>>` trigger: User wants inline completion without extra whitespace
+ * - `.=>` trigger: Alternative pattern for special cases
+ * - `schema` tag: Gets unique multi-line formatting with proper
indentation
+ *
+ * **Back Position Logic:**
+ * - `backpos`: Position of the '>' character itself (character - 1)
+ * - `backpos3`: Position 3 characters back (character - 3) for
detecting '>>' or '.=>'
+ * These positions are used to determine range deletion before snippet
insertion.
+ */
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
+ // Define the expected trigger character for validation
let triggerChar = '>'
+
+ // **GUARD CLAUSES**: Prevent completion in inappropriate contexts
+ // Similar to closeElementSlash.ts - ensures we don't interfere with
XPath, quoted values, etc.
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 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
the trigger char
) {
- return undefined
+ return undefined // Don't provide completion
}
+ // Initialize back positions for potential character removal
+ // backpos: position of the '>' character (will be adjusted to char -
1)
+ // backpos3: position 3 characters back (for detecting '>>' or '.=>'
patterns)
let backpos = position.with(position.line, position.character)
let backpos3 = position.with(position.line, position.character)
+ // Adjust backpos to point to the character just before '>'
if (position.character > 0) {
backpos = position.with(position.line, position.character - 1)
}
+ // Allow checking 3 characters back to detect special patterns
if (position.character > 2) {
backpos3 = position.with(position.line, position.character - 3)
}
+ // Get and preserve the namespace prefix (e.g., 'xs:', 'dfdl:', or
empty)
let nsPrefix = getNsPrefix(document, position)
const origPrefix = nsPrefix
+ // **CORE LOGIC**: Find the nearest unclosed tag that needs completion
+ // Returns 'none' if all tags are properly closed or self-closing
const nearestTagNotClosed = checkMissingCloseTag(
document,
position,
nsPrefix
)
+
+ // Adjust namespace prefix based on the specific tag type found
+ // Some tags always use 'dfdl:' prefix regardless of context
nsPrefix = getItemPrefix(nearestTagNotClosed, origPrefix)
+
+ // Get text from line start to cursor position for analysis
const triggerText = document
.lineAt(position)
.text.substring(0, position.character)
+ // Count how many XML items exist on the current line (important for
multi-tag scenarios)
let itemsOnLine = getItemsOnLineCount(triggerText)
+ // If no tags need closing, don't provide completion
if (nearestTagNotClosed.includes('none')) {
return undefined
}
+ // Initialize range for potential character deletion (to be updated
based on trigger)
let range = new vscode.Range(position, position)
+ // **MAIN TRIGGER CONDITIONS**: Determine what action to take based on
trigger pattern
+ // Three scenarios activate completion:
+ // 1. Single '>' on lines with < 2 items --> standard multi-line
completion
+ // 2. '>>' on lines with > 1 items --> inline completion without
whitespace
+ // 3. '.=>' on empty lines --> special case completion
if (
(triggerText.endsWith('>') && itemsOnLine < 2) ||
(triggerText.endsWith('>>') && itemsOnLine > 1) ||
(triggerText.endsWith('.=>') && itemsOnLine === 0)
) {
+ // Set range to delete the trigger character(s) before inserting
snippet
+ // This prevents duplication (e.g., inserting ">" when user already
typed it)
range = new vscode.Range(backpos, position)
+ // Perform the deletion of trigger character(s)
await vscode.window.activeTextEditor?.edit((editBuilder) => {
editBuilder.replace(range, '')
})
+ // **DELEGATE**: Complex insertion logic handled by checkItemsOnLine
+ // This function decides the exact snippet based on context
checkItemsOnLine(
document,
position,
@@ -107,13 +169,20 @@ export function getCloseElementProvider() {
backpos3
)
}
+
+ // Return undefined since completions are handled via direct snippet
insertion, not suggestions list
return undefined
},
},
- '>' // triggered whenever a '>' is typed
+ '>' // Register provider to trigger whenever a '>' is typed
)
}
+/**
+ * Registers a completion provider for TDML files
+ * Nearly identical to DFDL provider but without the `isNotTriggerChar`
validation
+ * TDML has simpler requirements and doesn't need as strict trigger validation
+ */
export function getTDMLCloseElementProvider() {
return vscode.languages.registerCompletionItemProvider(
'tdml',
@@ -122,6 +191,8 @@ export function getTDMLCloseElementProvider() {
document: vscode.TextDocument,
position: vscode.Position
) {
+ // **GUARD CLAUSES**: Same as DFDL but without trigger char validation
+ // TDML completion is slightly more permissive
if (
checkBraceOpen(document, position) ||
cursorWithinBraces(document, position) ||
@@ -132,6 +203,7 @@ export function getTDMLCloseElementProvider() {
return undefined
}
+ // Same back position logic as DFDL provider
let backpos = position.with(position.line, position.character)
let backpos3 = position.with(position.line, position.character)
@@ -143,6 +215,7 @@ export function getTDMLCloseElementProvider() {
backpos3 = position.with(position.line, position.character - 3)
}
+ // Namespace and tag detection (same logic as DFDL provider)
let nsPrefix = getNsPrefix(document, position)
const origPrefix = nsPrefix
@@ -158,23 +231,28 @@ export function getTDMLCloseElementProvider() {
let itemsOnLine = getItemsOnLineCount(triggerText)
+ // If no unclosed tags found, return early
if (nearestTagNotClosed.includes('none')) {
return undefined
}
let range = new vscode.Range(position, position)
+ // **SAME TRIGGER LOGIC**: Multi-tag lines use '>>', single-tag lines
use '>'
+ // '.=>' pattern also supported for empty lines
if (
(triggerText.endsWith('>') && itemsOnLine < 2) ||
(triggerText.endsWith('>>') && itemsOnLine > 1) ||
(triggerText.endsWith('.=>') && itemsOnLine === 0)
) {
+ // Set range to delete the trigger character(s)
range = new vscode.Range(backpos, position)
await vscode.window.activeTextEditor?.edit((editBuilder) => {
editBuilder.replace(range, '')
})
+ // Delegate to shared insertion logic
checkItemsOnLine(
document,
position,
@@ -187,13 +265,27 @@ export function getTDMLCloseElementProvider() {
backpos3
)
}
- //return undefined
+ //return undefined // Commented out but left in original code
},
},
- '>' // triggered whenever a '>' is typed
+ '>' // Register provider to trigger on '>'
)
}
+/**
+ * Core logic determining what snippet to insert when '>' is typed
+ * Handles three distinct scenarios based on items on the current line:
+ * - 0 items: Likely at document root level (e.g., closing schema tag)
+ * - 1 item: Standard element completion with proper formatting
+ * - >1 items: Complex multi-tag line requiring careful parsing
+ *
+ * **Snippet Strategy Variations:**
+ * - Container elements (schema, variables) get multi-line snippets with
indentation
+ * - Special tags (assert, discriminator) get inline snippets with tab stops
for expressions
+ * - Multi-tag lines get compact closing tags without extra whitespace
+ * - '>>' trigger means user wants inline completion without formatting
+ * - Standard '>' trigger means user wants properly formatted multi-line
completion
+ */
function checkItemsOnLine(
document: vscode.TextDocument,
position: vscode.Position,
@@ -205,22 +297,29 @@ function checkItemsOnLine(
backpos: vscode.Position,
backpos3: vscode.Position
) {
+ // **CASE 0: No items on line** - Likely at root level (e.g., schema element)
+ // Only proceed if no closing tags already present
if (
itemsOnLine == 0 &&
!triggerText.includes('</') &&
!triggerText.includes('/>')
) {
+ // Check if it's just a lone '>' character without any tag context
if (triggerText.trim() === '>') {
+ // Simple case: just insert the closing tag
insertSnippet('</' + nsPrefix + nearestTagNotClosed + '>', backpos)
} else {
+ // **SPECIAL: Schema tag gets unique multi-line treatment with proper
XML formatting**
switch (nearestTagNotClosed) {
case 'schema':
+ // '>>' triggers schema with extra indentation level for child
elements
if (triggerText.endsWith('>>')) {
insertSnippet(
'\n\t$0\n</' + nsPrefix + nearestTagNotClosed + '>',
backpos
)
} else {
+ // Standard schema completion with proper XML document structure
insertSnippet(
'>\n\t$0\n</' + nsPrefix + nearestTagNotClosed + '>',
backpos
@@ -228,6 +327,8 @@ function checkItemsOnLine(
}
break
default:
+ // **PATTERN RECOGNITION: '>>' means user wants inline completion**
+ // Single '>' means user wants multi-line completion with proper
indentation
if (triggerText.endsWith('>>')) {
insertSnippet(
'</' + nsPrefix + nearestTagNotClosed + '>$0',
@@ -244,11 +345,13 @@ function checkItemsOnLine(
}
}
+ // **CASE 1: Single item on line** - Standard element completion with proper
formatting
if (
itemsOnLine === 1 &&
!triggerText.includes('</') &&
!triggerText.includes('/>')
) {
+ // Delegate to specialized function for single-item scenarios
checkNearestTagNotClosed(
document,
position,
@@ -259,11 +362,19 @@ function checkItemsOnLine(
)
}
+ // **CASE 2: Multiple items on line** - Complex parsing required to avoid
breaking existing structure
if (itemsOnLine > 1) {
checkTriggerText(triggerText, nsPrefix, backpos, nearestTagNotClosed)
}
}
+/**
+ * Handles element completion for single-item lines
+ * Provides tag-specific snippet patterns based on DFDL conventions:
+ * - Variable tags (defineVariable, setVariable): Multi-line with newlines for
readability
+ * - Assertion tags (assert, discriminator): Inline snippets with tab stops
for test expressions
+ * - Default case: Multi-line container format with indentation for child
elements
+ */
function checkNearestTagNotClosed(
document: vscode.TextDocument,
position: vscode.Position,
@@ -274,23 +385,33 @@ function checkNearestTagNotClosed(
) {
const triggerText = document.lineAt(position.line).text
+ // **TAG-SPECIFIC LOGIC**: Different tags have different formatting
conventions in DFDL
switch (nearestTagNotClosed) {
+ // Variable-related tags typically span multiple lines with attributes
case 'defineVariable':
case 'setVariable':
insertSnippet('>\n</' + nsPrefix + nearestTagNotClosed + '>$0', backpos)
break
+
+ // Assertion tags are usually inline with test expressions between tags
case 'assert':
case 'discriminator':
if (triggerText.endsWith('>')) {
+ // If '>' already exists from user typing, just add closing tag with
tab stop
insertSnippet('$1</' + nsPrefix + nearestTagNotClosed + '>', backpos)
} else {
+ // Otherwise add both opening and closing tags with tab stops
insertSnippet('>$1</' + nsPrefix + nearestTagNotClosed + '>$0',
backpos)
}
break
+
+ // **DEFAULT CASE**: Most container tags get multi-line format with proper
indentation
default:
if (triggerText.trim() === '') {
+ // Empty line: just insert the closing tag
insertSnippet('</' + nsPrefix + nearestTagNotClosed + '>', backpos)
} else {
+ // Has opening tag: create container with newline and indentation for
children
insertSnippet(
'>\n\t$0\n</' + nsPrefix + nearestTagNotClosed + '>',
backpos
@@ -300,16 +421,28 @@ function checkNearestTagNotClosed(
}
}
+/**
+ * Handles element completion for multi-tag lines (itemsOnLine > 1)
+ * Carefully inspects the line to determine if a closing tag already exists
+ * Only inserts closing tag if one doesn't already exist to avoid duplication
+ * Supports both standard (></tag) and inline (>>) completion patterns
+ */
function checkTriggerText(
triggerText: string,
nsPrefix: string,
backpos: vscode.Position,
nearestTagNotClosed: string
) {
+ // Verify the specific tag we're trying to close actually exists on this line
if (triggerText.includes('<' + nsPrefix + nearestTagNotClosed)) {
let tagPos = triggerText.lastIndexOf('<' + nsPrefix + nearestTagNotClosed)
let tagEndPos = triggerText.indexOf('>', tagPos)
+ // **CONDITIONS FOR INSERTION**:
+ // 1. Tag exists on line, AND
+ // 2. It's not self-closing (no '/>'), AND
+ // 3. No closing tag already exists for it
+ // This prevents duplicate closing tags on multi-tag lines
if (
tagPos != -1 &&
!triggerText.substring(tagEndPos - 1, 2).includes('/>') &&
@@ -317,9 +450,11 @@ function checkTriggerText(
.substring(tagEndPos)
.includes('</' + nsPrefix + nearestTagNotClosed)
) {
+ // '>>' trigger: User already typed ">" so just add closing tag inline
if (triggerText.endsWith('>>')) {
insertSnippet('</' + nsPrefix + nearestTagNotClosed + '>$0', backpos)
} else {
+ // Standard: Add ">" + closing tag
insertSnippet('></' + nsPrefix + nearestTagNotClosed + '>$0', backpos)
}
}