node-red-contrib-knx-ultimate 7.0.1 → 7.0.2
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 +5 -0
- package/nodes/knxUltimate.html +45 -62
- package/package.json +2 -2
- package/resources/knxFunctionAutocomplete.js +223 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 7.0.2** - September 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **KNX Device — group address autocomplete**: both input and output function editors now suggest imported ETS group addresses in the first argument of `getGAValue(...)` and `setGAValue(...)`. Search by address, device name or DPT in Monaco and Ace.<br/>
|
|
12
|
+
- **KNX Device — readable GA references**: selecting an address adds its full ETS name and hierarchy to a deduplicated comment list at the beginning of the code. Clicking **Done** removes unused addresses from each editor's list before saving; addresses mentioned only in comments do not count as used.<br/>
|
|
13
|
+
|
|
9
14
|
**Version 7.0.1** - September 2026<br/>
|
|
10
15
|
|
|
11
16
|
KNX Device: fixed a wrong status text, when the node receives a READ request and responds with the last payload received.</br>
|
package/nodes/knxUltimate.html
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
|
|
26
26
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXSendSnippets.js"></script>
|
|
27
27
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXReceiveSnippets.js"></script>
|
|
28
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/knxFunctionAutocomplete.js"></script>
|
|
28
29
|
|
|
29
30
|
<script type="text/javascript">
|
|
30
31
|
RED.nodes.registerType('knxUltimate', {
|
|
@@ -407,6 +408,36 @@ return msg;`
|
|
|
407
408
|
})
|
|
408
409
|
}
|
|
409
410
|
|
|
411
|
+
let groupAddressRequest = null;
|
|
412
|
+
let groupAddressServerId = null;
|
|
413
|
+
let functionEditorsDisposed = false;
|
|
414
|
+
const functionEditorDisposables = [];
|
|
415
|
+
const loadFunctionGroupAddresses = () => {
|
|
416
|
+
const serverId = $("#node-input-server").val();
|
|
417
|
+
if (functionEditorsDisposed || !serverId || serverId === '_ADD_') return Promise.resolve([]);
|
|
418
|
+
if (!groupAddressRequest || groupAddressServerId !== serverId) {
|
|
419
|
+
groupAddressServerId = serverId;
|
|
420
|
+
groupAddressRequest = new Promise(resolve => {
|
|
421
|
+
$.getJSON("knxUltimatecsv?nodeID=" + encodeURIComponent(serverId))
|
|
422
|
+
.done(data => resolve(Array.isArray(data) ? data : []))
|
|
423
|
+
.fail(() => { groupAddressRequest = null; resolve([]); });
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return groupAddressRequest.then(data => {
|
|
427
|
+
return !functionEditorsDisposed && $("#node-input-server").val() === serverId ? data : [];
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
node.disposeKnxFunctionEditors = () => {
|
|
431
|
+
functionEditorsDisposed = true;
|
|
432
|
+
functionEditorDisposables.forEach(disposable => disposable.dispose());
|
|
433
|
+
['sampleEditor', 'sendMsgToKNXCodeEditor', 'receiveMsgFromKNXCodeEditor'].forEach(key => {
|
|
434
|
+
try { if (node[key]) node[key].destroy(); } catch (error) { }
|
|
435
|
+
delete node[key];
|
|
436
|
+
});
|
|
437
|
+
delete node.activeCodeEditor;
|
|
438
|
+
delete node.disposeKnxFunctionEditors;
|
|
439
|
+
};
|
|
440
|
+
|
|
410
441
|
const applyEditorOptions = (editor) => {
|
|
411
442
|
try {
|
|
412
443
|
if (!editor) return;
|
|
@@ -425,47 +456,15 @@ return msg;`
|
|
|
425
456
|
enableLiveAutocompletion: true
|
|
426
457
|
});
|
|
427
458
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
snippet: item.snippet,
|
|
436
|
-
meta: 'KNX helper',
|
|
437
|
-
doc: item.doc
|
|
438
|
-
}));
|
|
439
|
-
const helperCompleter = {
|
|
440
|
-
getCompletions: function (_editor, _session, _pos, prefix, callback) {
|
|
441
|
-
const search = (prefix || '').toLowerCase();
|
|
442
|
-
const filtered = search
|
|
443
|
-
? aceCompletions.filter(entry => entry.caption.toLowerCase().startsWith(search) || entry.value.toLowerCase().startsWith(search))
|
|
444
|
-
: aceCompletions;
|
|
445
|
-
callback(null, filtered.length ? filtered : aceCompletions);
|
|
446
|
-
},
|
|
447
|
-
getDocTooltip: function (item) {
|
|
448
|
-
if (!item || item.docHTML || !item.doc) return;
|
|
449
|
-
item.docHTML = '<b>' + item.caption + '</b><hr />' + item.doc;
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
editor.completers.push(helperCompleter);
|
|
453
|
-
editor._knxHelperCompleter = helperCompleter;
|
|
454
|
-
}
|
|
455
|
-
if (typeof monaco !== 'undefined' && !globalScope.knxFunctionMonacoCompletionProvider) {
|
|
459
|
+
functionEditorDisposables.push(globalScope.KNXUltimateFunctionAutocomplete.attach(editor, {
|
|
460
|
+
monaco: typeof monaco !== 'undefined' ? monaco : null,
|
|
461
|
+
ace: typeof ace !== 'undefined' ? ace : null,
|
|
462
|
+
loadGroupAddresses: loadFunctionGroupAddresses,
|
|
463
|
+
helperItems: knxFunctionHelperItems
|
|
464
|
+
}));
|
|
465
|
+
if (typeof monaco !== 'undefined' && !globalScope.knxFunctionMonacoGlobals) {
|
|
456
466
|
try {
|
|
457
|
-
|
|
458
|
-
label: item.label,
|
|
459
|
-
kind: monaco.languages.CompletionItemKind.Function,
|
|
460
|
-
insertText: item.snippet,
|
|
461
|
-
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
462
|
-
documentation: item.doc
|
|
463
|
-
}));
|
|
464
|
-
|
|
465
|
-
globalScope.knxFunctionMonacoCompletionProvider = monaco.languages.registerCompletionItemProvider('javascript', {
|
|
466
|
-
provideCompletionItems: () => ({ suggestions: functionSuggestions })
|
|
467
|
-
});
|
|
468
|
-
|
|
467
|
+
globalScope.knxFunctionMonacoGlobals = true;
|
|
469
468
|
monaco.languages.typescript.javascriptDefaults.addExtraLib([
|
|
470
469
|
'/** Read a KNX group address. This helper is async, so use await to get the real value. By default, if the value is not cached, a KNX read is sent. Pass false as third parameter to use cache-only mode. */',
|
|
471
470
|
'declare function getGAValue(address: string, dptOrReadIfMissing?: string | boolean, readIfMissing?: boolean): Promise<any>;',
|
|
@@ -764,6 +763,7 @@ return msg;`
|
|
|
764
763
|
})
|
|
765
764
|
|
|
766
765
|
function checkUI() {
|
|
766
|
+
groupAddressRequest = null;
|
|
767
767
|
|
|
768
768
|
// Backward compatibility
|
|
769
769
|
if (node.outputRBE === true || $("#node-input-outputRBE").val() === true) {
|
|
@@ -1117,8 +1117,9 @@ return msg;`
|
|
|
1117
1117
|
} catch (error) { }
|
|
1118
1118
|
|
|
1119
1119
|
var node = this;
|
|
1120
|
-
|
|
1121
|
-
this.
|
|
1120
|
+
const cleanFunctionCode = window.KNXUltimateFunctionAutocomplete.pruneUnusedGaComments;
|
|
1121
|
+
this.sendMsgToKNXCode = cleanFunctionCode(this.sendMsgToKNXCodeEditor.getValue());
|
|
1122
|
+
this.receiveMsgFromKNXCode = cleanFunctionCode(this.receiveMsgFromKNXCodeEditor.getValue());
|
|
1122
1123
|
if ($("#node-input-setTopicType").val() === "listenAllGA") {
|
|
1123
1124
|
this.listenallga = true;
|
|
1124
1125
|
} else {
|
|
@@ -1150,16 +1151,7 @@ return msg;`
|
|
|
1150
1151
|
// $("#divInputRBE").show()
|
|
1151
1152
|
}
|
|
1152
1153
|
|
|
1153
|
-
|
|
1154
|
-
try {
|
|
1155
|
-
node.sampleEditor.destroy();
|
|
1156
|
-
delete node.sampleEditor;
|
|
1157
|
-
node.sendMsgToKNXCodeEditor.destroy();
|
|
1158
|
-
delete node.sendMsgToKNXCodeEditor;
|
|
1159
|
-
node.receiveMsgFromKNXCodeEditor.destroy();
|
|
1160
|
-
delete node.receiveMsgFromKNXCodeEditor;
|
|
1161
|
-
//RED.editor.destroy(); // 23/01/2024 added
|
|
1162
|
-
} catch (error) { }
|
|
1154
|
+
if (node.disposeKnxFunctionEditors) node.disposeKnxFunctionEditors();
|
|
1163
1155
|
|
|
1164
1156
|
},
|
|
1165
1157
|
oneditcancel: function () {
|
|
@@ -1168,16 +1160,7 @@ return msg;`
|
|
|
1168
1160
|
RED.sidebar.show("info");
|
|
1169
1161
|
} catch (error) { }
|
|
1170
1162
|
|
|
1171
|
-
|
|
1172
|
-
try {
|
|
1173
|
-
node.sampleEditor.destroy();
|
|
1174
|
-
delete node.sampleEditor;
|
|
1175
|
-
node.sendMsgToKNXCodeEditor.destroy();
|
|
1176
|
-
delete node.sendMsgToKNXCodeEditor;
|
|
1177
|
-
node.receiveMsgFromKNXCodeEditor.destroy();
|
|
1178
|
-
delete node.receiveMsgFromKNXCodeEditor;
|
|
1179
|
-
RED.editor.destroy(); // 23/01/2024 added
|
|
1180
|
-
} catch (error) { }
|
|
1163
|
+
if (this.disposeKnxFunctionEditors) this.disposeKnxFunctionEditors();
|
|
1181
1164
|
}
|
|
1182
1165
|
})
|
|
1183
1166
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "7.0.
|
|
6
|
+
"version": "7.0.2",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT and Modbus adapters, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -152,4 +152,4 @@
|
|
|
152
152
|
"vite": "^7.3.6",
|
|
153
153
|
"vue": "^3.5.41"
|
|
154
154
|
}
|
|
155
|
-
}
|
|
155
|
+
}
|
|
@@ -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
|
+
}))
|