node-red-contrib-knx-ultimate 7.0.1 → 7.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/nodes/knxUltimate.html +45 -62
- package/nodes/locales/it/hue-config.html +3 -3
- package/nodes/locales/it/knxUltimate-config.html +15 -15
- package/nodes/locales/it/knxUltimate-config.json +7 -7
- package/nodes/locales/it/knxUltimate.html +12 -12
- package/nodes/locales/it/knxUltimate.json +19 -19
- package/nodes/locales/it/knxUltimateAI.json +5 -5
- package/nodes/locales/it/knxUltimateAlerter.json +3 -3
- package/nodes/locales/it/knxUltimateAutoResponder.html +11 -11
- package/nodes/locales/it/knxUltimateGlobalContext.html +1 -1
- package/nodes/locales/it/knxUltimateGlobalContext.json +2 -2
- package/nodes/locales/it/knxUltimateHATranslator.html +4 -5
- package/nodes/locales/it/knxUltimateHueBattery.json +2 -2
- package/nodes/locales/it/knxUltimateHueButton.html +1 -1
- package/nodes/locales/it/knxUltimateHueCameraMotion.html +4 -4
- package/nodes/locales/it/knxUltimateHueCameraMotion.json +6 -6
- package/nodes/locales/it/knxUltimateHueContactSensor.html +2 -2
- package/nodes/locales/it/knxUltimateHueController.html +51 -52
- package/nodes/locales/it/knxUltimateHueHumiditySensor.html +1 -1
- package/nodes/locales/it/knxUltimateHueHumiditySensor.json +2 -2
- package/nodes/locales/it/knxUltimateHueLight.html +33 -33
- package/nodes/locales/it/knxUltimateHueLight.json +6 -6
- package/nodes/locales/it/knxUltimateHueLightSensor.html +1 -1
- package/nodes/locales/it/knxUltimateHuePlug.html +5 -5
- package/nodes/locales/it/knxUltimateHuePlug.json +1 -1
- package/nodes/locales/it/knxUltimateHueScene.html +1 -1
- package/nodes/locales/it/knxUltimateHueScene.json +1 -1
- package/nodes/locales/it/knxUltimateHueTapDial.json +2 -2
- package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +1 -1
- package/nodes/locales/it/knxUltimateHueTemperatureSensor.json +1 -1
- package/nodes/locales/it/knxUltimateHuedevice_software_update.html +2 -3
- package/nodes/locales/it/knxUltimateIoTBridge.json +1 -1
- package/nodes/locales/it/knxUltimateLoadControl.html +12 -12
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +5 -5
- package/nodes/locales/it/knxUltimateRouterFilter.html +18 -18
- package/nodes/locales/it/knxUltimateRouterFilter.json +10 -10
- package/nodes/locales/it/knxUltimateSceneController.html +6 -6
- package/nodes/locales/it/knxUltimateSceneController.json +5 -5
- package/nodes/locales/it/knxUltimateStaircase.html +1 -1
- package/nodes/locales/it/knxUltimateStaircase.json +2 -2
- package/nodes/locales/it/knxUltimateWatchDog.html +2 -2
- package/nodes/locales/it/knxUltimateWatchDog.json +2 -2
- package/package.json +2 -2
- package/resources/hueControllerProfiles.js +1 -1
- package/resources/knxFunctionAutocomplete.js +223 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory()
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api
|
|
4
|
+
if (root) root.KNXUltimateFunctionAutocomplete = api
|
|
5
|
+
}(typeof window !== 'undefined' ? window : globalThis, function () {
|
|
6
|
+
const firstArgument = /(?:^|[^\w$.])(?:getGAValue|setGAValue)\s*\(\s*$/
|
|
7
|
+
|
|
8
|
+
// Scan strings and comments so helper names in examples/comments cannot trigger a GA edit.
|
|
9
|
+
function getContext (source, offset) {
|
|
10
|
+
let code = ''
|
|
11
|
+
let index = 0
|
|
12
|
+
while (index < offset) {
|
|
13
|
+
const char = source[index]
|
|
14
|
+
if (source.slice(index, index + 2) === '//') {
|
|
15
|
+
const end = source.indexOf('\n', index + 2)
|
|
16
|
+
if (end < 0 || end >= offset) return null
|
|
17
|
+
code += ' '
|
|
18
|
+
index = end
|
|
19
|
+
} else if (source.slice(index, index + 2) === '/*') {
|
|
20
|
+
const end = source.indexOf('*/', index + 2)
|
|
21
|
+
if (end < 0 || end + 2 > offset) return null
|
|
22
|
+
code += ' '
|
|
23
|
+
index = end + 2
|
|
24
|
+
} else if (char === "'" || char === '"' || char === '`') {
|
|
25
|
+
const start = index + 1
|
|
26
|
+
let end = start
|
|
27
|
+
while (end < source.length && source[end] !== char && source[end] !== '\n') {
|
|
28
|
+
if (source[end] === '\\') end++
|
|
29
|
+
end++
|
|
30
|
+
}
|
|
31
|
+
if (offset <= end) {
|
|
32
|
+
if (!firstArgument.test(code) || (char === '`' && source.slice(start, end).includes('${'))) return null
|
|
33
|
+
return { start, end, query: source.slice(start, offset), quote: char, closed: source[end] === char }
|
|
34
|
+
}
|
|
35
|
+
code += ' literal '
|
|
36
|
+
index = end + 1
|
|
37
|
+
// Template literals can span lines; skip the rest rather than treating their text as code.
|
|
38
|
+
if (char === '`' && source[end] !== char) {
|
|
39
|
+
while (index < source.length && source[index] !== '`') {
|
|
40
|
+
if (source[index] === '\\') index++
|
|
41
|
+
index++
|
|
42
|
+
}
|
|
43
|
+
if (index >= offset) return null
|
|
44
|
+
index++
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
code += char
|
|
48
|
+
index++
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return firstArgument.test(code) ? { start: offset, end: offset, query: '', quote: '', closed: false } : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getEntries (data, query) {
|
|
55
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean)
|
|
56
|
+
const seen = new Set()
|
|
57
|
+
return (Array.isArray(data) ? data : []).filter(entry => {
|
|
58
|
+
if (!entry || typeof entry.ga !== 'string' || !entry.ga || seen.has(entry.ga)) return false
|
|
59
|
+
seen.add(entry.ga)
|
|
60
|
+
return terms.every(term => `${entry.ga} ${entry.devicename || ''} ${entry.dpt || ''}`.toLowerCase().includes(term))
|
|
61
|
+
}).map(entry => ({
|
|
62
|
+
ga: entry.ga,
|
|
63
|
+
comment: '// ' + `${entry.ga} ${entry.devicename || ''}`.replace(/[\r\n\u2028\u2029]+/g, ' ').trim(),
|
|
64
|
+
label: `${entry.ga} # ${entry.devicename || ''} # ${entry.dpt || ''}`
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function replacement (context, ga) {
|
|
69
|
+
return context.quote ? ga + (context.closed ? '' : context.quote) : "'" + ga + "'"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getGaHeader (source) {
|
|
73
|
+
const entries = []
|
|
74
|
+
let end = 0
|
|
75
|
+
let match
|
|
76
|
+
const line = /^[\t ]*\/\/[\t ]+(\d+(?:\/\d+){0,2})(?:[\t ]+[^\r\n]*)?(?:\r?\n|$)/
|
|
77
|
+
while ((match = source.slice(end).match(line))) {
|
|
78
|
+
entries.push({ ga: match[1], comment: match[0].trim(), start: end, end: end + match[0].length })
|
|
79
|
+
end += match[0].length
|
|
80
|
+
}
|
|
81
|
+
return { entries, end }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getCommentEdit (source, entry) {
|
|
85
|
+
const header = getGaHeader(source)
|
|
86
|
+
const existing = header.entries.find(item => item.ga === entry.ga)
|
|
87
|
+
if (existing && existing.comment === entry.comment) return null
|
|
88
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n'
|
|
89
|
+
return { start: existing ? existing.start : header.end, end: existing ? existing.end : header.end, text: entry.comment + eol }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pruneUnusedGaComments (source) {
|
|
93
|
+
const header = getGaHeader(source)
|
|
94
|
+
if (!header.entries.length) return source
|
|
95
|
+
const body = source.slice(header.end)
|
|
96
|
+
// Preserve quoted strings while removing disabled code and explanatory comments.
|
|
97
|
+
const code = body.replace(/"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)/g, token => {
|
|
98
|
+
return token.startsWith('//') || token.startsWith('/*') ? ' ' : token
|
|
99
|
+
}).replace(/\\\//g, '/')
|
|
100
|
+
const seen = new Set()
|
|
101
|
+
const kept = header.entries.filter(entry => {
|
|
102
|
+
if (seen.has(entry.ga) || !new RegExp('(^|[^\\w/])' + entry.ga + '(?![\\w/])').test(code)) return false
|
|
103
|
+
seen.add(entry.ga)
|
|
104
|
+
return true
|
|
105
|
+
})
|
|
106
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n'
|
|
107
|
+
return kept.map(entry => entry.comment + eol).join('') + body
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function attach (editor, { monaco, ace, loadGroupAddresses, helperItems }) {
|
|
111
|
+
if (monaco && typeof editor.getModel === 'function') {
|
|
112
|
+
editor.updateOptions({ quickSuggestions: { other: true, comments: false, strings: true }, suggestOnTriggerCharacters: true })
|
|
113
|
+
return monaco.languages.registerCompletionItemProvider('javascript', {
|
|
114
|
+
triggerCharacters: ['(', "'", '"', '`', '/'],
|
|
115
|
+
provideCompletionItems: async (model, position, _context, token) => {
|
|
116
|
+
if (model !== editor.getModel()) return { suggestions: [] }
|
|
117
|
+
const context = getContext(model.getValue(), model.getOffsetAt(position))
|
|
118
|
+
if (!context) {
|
|
119
|
+
const word = model.getWordUntilPosition(position)
|
|
120
|
+
return {
|
|
121
|
+
suggestions: helperItems.map(item => ({
|
|
122
|
+
label: item.label,
|
|
123
|
+
kind: monaco.languages.CompletionItemKind.Function,
|
|
124
|
+
insertText: item.snippet,
|
|
125
|
+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
126
|
+
documentation: item.doc,
|
|
127
|
+
range: new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn)
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const entries = getEntries(await loadGroupAddresses(), context.query)
|
|
132
|
+
if ((token && token.isCancellationRequested) || model.isDisposed() || model !== editor.getModel()) return { suggestions: [] }
|
|
133
|
+
const start = model.getPositionAt(context.start)
|
|
134
|
+
const end = model.getPositionAt(context.end)
|
|
135
|
+
return {
|
|
136
|
+
incomplete: true,
|
|
137
|
+
suggestions: entries.map(entry => {
|
|
138
|
+
const comment = getCommentEdit(model.getValue(), entry)
|
|
139
|
+
const suggestion = {
|
|
140
|
+
label: entry.label,
|
|
141
|
+
kind: monaco.languages.CompletionItemKind.Value,
|
|
142
|
+
insertText: replacement(context, entry.ga),
|
|
143
|
+
// The list is already filtered by every search term, including ETS names.
|
|
144
|
+
filterText: context.query + ' ' + entry.label,
|
|
145
|
+
detail: 'KNX group address',
|
|
146
|
+
range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column)
|
|
147
|
+
}
|
|
148
|
+
if (comment) {
|
|
149
|
+
const position = model.getPositionAt(comment.start)
|
|
150
|
+
const end = model.getPositionAt(comment.end)
|
|
151
|
+
suggestion.additionalTextEdits = [{
|
|
152
|
+
range: new monaco.Range(position.lineNumber, position.column, end.lineNumber, end.column),
|
|
153
|
+
text: comment.text
|
|
154
|
+
}]
|
|
155
|
+
}
|
|
156
|
+
return suggestion
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!ace || !ace.require) return { dispose () {} }
|
|
164
|
+
const Range = ace.require('ace/range').Range
|
|
165
|
+
const getAceContext = (session, position) => getContext(session.getValue(), session.getDocument().positionToIndex(position))
|
|
166
|
+
const helpers = {
|
|
167
|
+
getCompletions (_editor, session, position, prefix, callback) {
|
|
168
|
+
if (getAceContext(session, position)) return callback(null, [])
|
|
169
|
+
const search = (prefix || '').toLowerCase()
|
|
170
|
+
callback(null, helperItems.filter(item => item.id.toLowerCase().startsWith(search)).map(item => ({
|
|
171
|
+
caption: item.label, value: item.aceValue, snippet: item.snippet, meta: 'KNX helper', docText: item.doc
|
|
172
|
+
})))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const addresses = {
|
|
176
|
+
identifierRegexps: [/[a-zA-Z0-9_$\/\u00A2-\uFFFF]/],
|
|
177
|
+
getCompletions (_editor, session, position, _prefix, callback) {
|
|
178
|
+
const context = getAceContext(session, position)
|
|
179
|
+
if (!context) return callback(null, [])
|
|
180
|
+
loadGroupAddresses().then(data => callback(null, getEntries(data, context.query).map(entry => ({
|
|
181
|
+
caption: entry.label, value: entry.ga, comment: entry.comment, meta: 'KNX GA', score: 1000, completer: addresses
|
|
182
|
+
}))), () => callback(null, []))
|
|
183
|
+
},
|
|
184
|
+
insertMatch (target, item) {
|
|
185
|
+
const context = getAceContext(target.session, target.getCursorPosition())
|
|
186
|
+
if (!context) return
|
|
187
|
+
const doc = target.session.getDocument()
|
|
188
|
+
const start = doc.indexToPosition(context.start)
|
|
189
|
+
const end = doc.indexToPosition(context.end)
|
|
190
|
+
const text = replacement(context, item.value)
|
|
191
|
+
const comment = getCommentEdit(target.session.getValue(), { ga: item.value, comment: item.comment })
|
|
192
|
+
target.session.replace(new Range(start.row, start.column, end.row, end.column), text)
|
|
193
|
+
if (comment) {
|
|
194
|
+
const position = doc.indexToPosition(comment.start)
|
|
195
|
+
const end = doc.indexToPosition(comment.end)
|
|
196
|
+
target.session.replace(new Range(position.row, position.column, end.row, end.column), comment.text)
|
|
197
|
+
}
|
|
198
|
+
target.clearSelection()
|
|
199
|
+
target.moveCursorToPosition(doc.indexToPosition(context.start + text.length + (comment ? comment.text.length - (comment.end - comment.start) : 0)))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Ace's default completer array is shared across editor instances.
|
|
203
|
+
editor.completers = (editor.completers || []).concat(helpers, addresses)
|
|
204
|
+
const afterExec = event => {
|
|
205
|
+
if (event.command.name !== 'insertstring' || !['(', "'", '"', '`', '/', ' '].includes(event.args)) return
|
|
206
|
+
if (!getAceContext(editor.session, editor.getCursorPosition())) return
|
|
207
|
+
const Autocomplete = ace.require('ace/autocomplete').Autocomplete
|
|
208
|
+
const completer = editor.completer || new Autocomplete()
|
|
209
|
+
editor.completer = completer
|
|
210
|
+
completer.autoInsert = false
|
|
211
|
+
completer.showPopup(editor)
|
|
212
|
+
}
|
|
213
|
+
editor.commands.on('afterExec', afterExec)
|
|
214
|
+
return {
|
|
215
|
+
dispose () {
|
|
216
|
+
editor.commands.off('afterExec', afterExec)
|
|
217
|
+
editor.completers = editor.completers.filter(item => item !== helpers && item !== addresses)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { getContext, getEntries, attach, pruneUnusedGaComments }
|
|
223
|
+
}))
|