This is an automated email from the ASF dual-hosted git repository.

rthomas320 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 7285b33  Intellisense: Don't suggest attributes if they already exist 
in an element
7285b33 is described below

commit 7285b33536861c8b4e4e77439e00c4bf7443c246
Author: Jeremy Yao <[email protected]>
AuthorDate: Wed Jul 9 10:15:01 2025 -0400

    Intellisense: Don't suggest attributes if they already exist in an element
    
    Closes #1199
---
 src/language/providers/attributeCompletion.ts | 157 ++++++++++++++++++++++++--
 1 file changed, 150 insertions(+), 7 deletions(-)

diff --git a/src/language/providers/attributeCompletion.ts 
b/src/language/providers/attributeCompletion.ts
index c16a04c..806e102 100644
--- a/src/language/providers/attributeCompletion.ts
+++ b/src/language/providers/attributeCompletion.ts
@@ -16,7 +16,7 @@
  */
 
 import * as vscode from 'vscode'
-
+import { xml2js } from 'xml-js'
 import {
   XmlItem,
   getSchemaNsPrefix,
@@ -67,6 +67,144 @@ function getCompletionItems(
   return compItems
 }
 
+/** Retrieves relevant lines of the document for use in 
prunedDuplicateAttributes
+ * Format of return is as follows [relevant parts of the string, index 
representing the location of the cursor in the string]
+ *
+ * @param position
+ * @param document
+ * @returns
+ */
+function getPotentialAttributeText(
+  position: vscode.Position,
+  document: vscode.TextDocument
+): [string, number] {
+  // Overall strategy: Find the lines that are relevant to the XML element 
we're looking at. The element can be incomplete and not closed.
+  let lowerLineBound: number = position.line
+  let upperLineBound: number = position.line
+
+  // Determining the lowerbound strategy: Traverse backwards line-by-line 
until we encounter an opening character (<)
+  while (
+    lowerLineBound > 0 && // Make sure we aren't going to negative line indexes
+    document.lineAt(lowerLineBound).text.indexOf('<') == -1 // continue going 
up the document if there is no <
+  ) {
+    lowerLineBound-- // traverse backwards via decrementing line index
+  }
+
+  // Upperbound strategy: Increment the upperLineBound 1 line downward to 
avoid the edge case it's equal to the lowerLineBound and there's more content 
beyond lowerLineBound
+  if (upperLineBound != document.lineCount - 1) {
+    upperLineBound++
+  }
+
+  // then, check the subsequent lines if there is an opening character (<)
+  while (
+    upperLineBound != document.lineCount - 1 &&
+    document.lineAt(upperLineBound).text.indexOf('<') == -1
+  ) {
+    upperLineBound++
+  }
+
+  let joinedStr = ''
+  let cursorIndexInStr = -1
+  // start joining the lines from lowerLineBound to upperLineBound
+  for (
+    let currLineIndex: number = lowerLineBound;
+    currLineIndex <= upperLineBound;
+    currLineIndex++
+  ) {
+    const currLine: string = document.lineAt(currLineIndex).text
+
+    if (currLineIndex == position.line) {
+      // note where the cursor is placed as an index relative to the fully 
joined string
+      cursorIndexInStr = joinedStr.length + position.character + -1
+    }
+
+    joinedStr += currLine + '\n'
+  }
+
+  return [joinedStr, cursorIndexInStr]
+}
+
+/** Removes duplicate attribute suggestions from an element. Also handles 
cases where the element is prefixed with dfdl:
+ *
+ * @param originalAttributeSuggestions  The completion item list
+ * @param position position object provided by VSCode of the cursor
+ * @param document vscode object
+ * @param nsPrefix namespace prefix of the element (includes the :)
+ * @returns
+ */
+function prunedDuplicateAttributes(
+  originalAttributeSuggestions: vscode.CompletionItem[] | undefined,
+  position: vscode.Position,
+  document: vscode.TextDocument,
+  nsPrefix: string
+): vscode.CompletionItem[] | undefined {
+  if (
+    originalAttributeSuggestions == undefined ||
+    originalAttributeSuggestions.length == 0
+  ) {
+    return originalAttributeSuggestions
+  }
+
+  const relevantJoinedLinesOfTextItems = getPotentialAttributeText(
+    position,
+    document
+  )
+  const textIndex = 0
+  const cursorPosIndex = 1
+
+  // Setting up stuff to create a full string representation of the XML element
+  const relevantDocText = relevantJoinedLinesOfTextItems[textIndex]
+  let indexLowerBound = relevantJoinedLinesOfTextItems[cursorPosIndex] // This 
gets the character right behind the cursor
+  let indexUpperBound = indexLowerBound + 1 // This gets the character after 
the cursor
+
+  // Traverse backwards character by character to find the first <
+  while (indexLowerBound >= 1 && relevantDocText[indexLowerBound] != '<') {
+    indexLowerBound--
+  }
+
+  // Traverse forward character by character to find > or <
+  while (
+    indexUpperBound < relevantDocText.length - 1 &&
+    !(
+      relevantDocText[indexUpperBound] == '<' ||
+      relevantDocText[indexUpperBound] == '>'
+    )
+  ) {
+    indexUpperBound++
+  }
+
+  // Create the full representation of the current XML element for parsing
+  // Force it to be closed if the current xml element isn't closed it
+  const fullXMLElementText =
+    relevantDocText[indexUpperBound - 1] != '>'
+      ? `${relevantDocText.substring(indexLowerBound, indexUpperBound - 1)}>`
+      : relevantDocText.substring(indexLowerBound, indexUpperBound)
+
+  // Obtain attributes for the currentl XML element after attempting to parse 
the whole thing as an XML element
+  const xmlRep = xml2js(fullXMLElementText, {})
+  const attributes = xmlRep.elements?.[0].attributes
+
+  if (attributes) {
+    // Some autocompletion attributes may or may not contain the dfdl: 
attribute when you accept it
+    // This flag determines whether or not we should ignore the dfdl: label 
when looking at the original attribute suggestions
+    const removeDFDLPrefix = nsPrefix === 'dfdl:'
+    const attributeSet: Set<string> = new Set(Object.keys(attributes))
+
+    // Return attributes that don't exist in the orignal all encompassing list
+    // Note if the element has a dfdl: prefix, then only look at the suffix of 
the attribute
+    return originalAttributeSuggestions.filter((suggestionItem) => {
+      const SuggestionLabel = suggestionItem.label.toString()
+      return !attributeSet.has(
+        removeDFDLPrefix && SuggestionLabel.startsWith('dfdl:')
+          ? SuggestionLabel.substring('dfdl:'.length)
+          : SuggestionLabel
+      )
+    })
+  }
+
+  return originalAttributeSuggestions
+}
+
 export function getAttributeCompletionProvider() {
   return vscode.languages.registerCompletionItemProvider(
     { language: 'dfdl' },
@@ -107,8 +245,7 @@ export function getAttributeCompletionProvider() {
           itemsOnLine < 2
             ? '\t'
             : ''
-
-        return checkNearestOpenItem(
+        const fullAttrCompletionList = checkNearestOpenItem(
           nearestOpenItem,
           triggerText,
           nsPrefix,
@@ -117,6 +254,13 @@ export function getAttributeCompletionProvider() {
           charBeforeTrigger,
           charAfterTrigger
         )
+
+        return prunedDuplicateAttributes(
+          fullAttrCompletionList,
+          position,
+          document,
+          nsPrefix
+        )
       },
     },
     ' ',
@@ -229,10 +373,9 @@ function checkNearestOpenItem(
     charAfterTrigger !== '\t'
       ? ' '
       : ''
-  let dfdlPrefix = dfdlDefaultPrefix
-  if (nsPrefix === 'dfdl:') {
-    dfdlPrefix = ''
-  }
+
+  const dfdlPrefix = nsPrefix === 'dfdl:' ? '' : dfdlDefaultPrefix
+
   switch (nearestOpenItem) {
     case 'element':
       return getCompletionItems(

Reply via email to