Skip to content

Commit 7285b33

Browse files
JeremyYaorthomas320
authored andcommitted
Intellisense: Don't suggest attributes if they already exist in an element
Closes apache#1199
1 parent 253d650 commit 7285b33

File tree

1 file changed

+150
-7
lines changed

1 file changed

+150
-7
lines changed

src/language/providers/attributeCompletion.ts

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import * as vscode from 'vscode'
19-
19+
import { xml2js } from 'xml-js'
2020
import {
2121
XmlItem,
2222
getSchemaNsPrefix,
@@ -67,6 +67,144 @@ function getCompletionItems(
6767
return compItems
6868
}
6969

70+
/** Retrieves relevant lines of the document for use in prunedDuplicateAttributes
71+
* Format of return is as follows [relevant parts of the string, index representing the location of the cursor in the string]
72+
*
73+
* @param position
74+
* @param document
75+
* @returns
76+
*/
77+
function getPotentialAttributeText(
78+
position: vscode.Position,
79+
document: vscode.TextDocument
80+
): [string, number] {
81+
// Overall strategy: Find the lines that are relevant to the XML element we're looking at. The element can be incomplete and not closed.
82+
let lowerLineBound: number = position.line
83+
let upperLineBound: number = position.line
84+
85+
// Determining the lowerbound strategy: Traverse backwards line-by-line until we encounter an opening character (<)
86+
while (
87+
lowerLineBound > 0 && // Make sure we aren't going to negative line indexes
88+
document.lineAt(lowerLineBound).text.indexOf('<') == -1 // continue going up the document if there is no <
89+
) {
90+
lowerLineBound-- // traverse backwards via decrementing line index
91+
}
92+
93+
// 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
94+
if (upperLineBound != document.lineCount - 1) {
95+
upperLineBound++
96+
}
97+
98+
// then, check the subsequent lines if there is an opening character (<)
99+
while (
100+
upperLineBound != document.lineCount - 1 &&
101+
document.lineAt(upperLineBound).text.indexOf('<') == -1
102+
) {
103+
upperLineBound++
104+
}
105+
106+
let joinedStr = ''
107+
let cursorIndexInStr = -1
108+
// start joining the lines from lowerLineBound to upperLineBound
109+
for (
110+
let currLineIndex: number = lowerLineBound;
111+
currLineIndex <= upperLineBound;
112+
currLineIndex++
113+
) {
114+
const currLine: string = document.lineAt(currLineIndex).text
115+
116+
if (currLineIndex == position.line) {
117+
// note where the cursor is placed as an index relative to the fully joined string
118+
cursorIndexInStr = joinedStr.length + position.character + -1
119+
}
120+
121+
joinedStr += currLine + '\n'
122+
}
123+
124+
return [joinedStr, cursorIndexInStr]
125+
}
126+
127+
/** Removes duplicate attribute suggestions from an element. Also handles cases where the element is prefixed with dfdl:
128+
*
129+
* @param originalAttributeSuggestions The completion item list
130+
* @param position position object provided by VSCode of the cursor
131+
* @param document vscode object
132+
* @param nsPrefix namespace prefix of the element (includes the :)
133+
* @returns
134+
*/
135+
function prunedDuplicateAttributes(
136+
originalAttributeSuggestions: vscode.CompletionItem[] | undefined,
137+
position: vscode.Position,
138+
document: vscode.TextDocument,
139+
nsPrefix: string
140+
): vscode.CompletionItem[] | undefined {
141+
if (
142+
originalAttributeSuggestions == undefined ||
143+
originalAttributeSuggestions.length == 0
144+
) {
145+
return originalAttributeSuggestions
146+
}
147+
148+
const relevantJoinedLinesOfTextItems = getPotentialAttributeText(
149+
position,
150+
document
151+
)
152+
const textIndex = 0
153+
const cursorPosIndex = 1
154+
155+
// Setting up stuff to create a full string representation of the XML element
156+
const relevantDocText = relevantJoinedLinesOfTextItems[textIndex]
157+
let indexLowerBound = relevantJoinedLinesOfTextItems[cursorPosIndex] // This gets the character right behind the cursor
158+
let indexUpperBound = indexLowerBound + 1 // This gets the character after the cursor
159+
160+
// Traverse backwards character by character to find the first <
161+
while (indexLowerBound >= 1 && relevantDocText[indexLowerBound] != '<') {
162+
indexLowerBound--
163+
}
164+
165+
// Traverse forward character by character to find > or <
166+
while (
167+
indexUpperBound < relevantDocText.length - 1 &&
168+
!(
169+
relevantDocText[indexUpperBound] == '<' ||
170+
relevantDocText[indexUpperBound] == '>'
171+
)
172+
) {
173+
indexUpperBound++
174+
}
175+
176+
// Create the full representation of the current XML element for parsing
177+
// Force it to be closed if the current xml element isn't closed it
178+
const fullXMLElementText =
179+
relevantDocText[indexUpperBound - 1] != '>'
180+
? `${relevantDocText.substring(indexLowerBound, indexUpperBound - 1)}>`
181+
: relevantDocText.substring(indexLowerBound, indexUpperBound)
182+
183+
// Obtain attributes for the currentl XML element after attempting to parse the whole thing as an XML element
184+
const xmlRep = xml2js(fullXMLElementText, {})
185+
const attributes = xmlRep.elements?.[0].attributes
186+
187+
if (attributes) {
188+
// Some autocompletion attributes may or may not contain the dfdl: attribute when you accept it
189+
// This flag determines whether or not we should ignore the dfdl: label when looking at the original attribute suggestions
190+
const removeDFDLPrefix = nsPrefix === 'dfdl:'
191+
const attributeSet: Set<string> = new Set(Object.keys(attributes))
192+
193+
// Return attributes that don't exist in the orignal all encompassing list
194+
// Note if the element has a dfdl: prefix, then only look at the suffix of the attribute
195+
return originalAttributeSuggestions.filter((suggestionItem) => {
196+
const SuggestionLabel = suggestionItem.label.toString()
197+
return !attributeSet.has(
198+
removeDFDLPrefix && SuggestionLabel.startsWith('dfdl:')
199+
? SuggestionLabel.substring('dfdl:'.length)
200+
: SuggestionLabel
201+
)
202+
})
203+
}
204+
205+
return originalAttributeSuggestions
206+
}
207+
70208
export function getAttributeCompletionProvider() {
71209
return vscode.languages.registerCompletionItemProvider(
72210
{ language: 'dfdl' },
@@ -107,8 +245,7 @@ export function getAttributeCompletionProvider() {
107245
itemsOnLine < 2
108246
? '\t'
109247
: ''
110-
111-
return checkNearestOpenItem(
248+
const fullAttrCompletionList = checkNearestOpenItem(
112249
nearestOpenItem,
113250
triggerText,
114251
nsPrefix,
@@ -117,6 +254,13 @@ export function getAttributeCompletionProvider() {
117254
charBeforeTrigger,
118255
charAfterTrigger
119256
)
257+
258+
return prunedDuplicateAttributes(
259+
fullAttrCompletionList,
260+
position,
261+
document,
262+
nsPrefix
263+
)
120264
},
121265
},
122266
' ',
@@ -229,10 +373,9 @@ function checkNearestOpenItem(
229373
charAfterTrigger !== '\t'
230374
? ' '
231375
: ''
232-
let dfdlPrefix = dfdlDefaultPrefix
233-
if (nsPrefix === 'dfdl:') {
234-
dfdlPrefix = ''
235-
}
376+
377+
const dfdlPrefix = nsPrefix === 'dfdl:' ? '' : dfdlDefaultPrefix
378+
236379
switch (nearestOpenItem) {
237380
case 'element':
238381
return getCompletionItems(

0 commit comments

Comments
 (0)