1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
2 // Distributed under an MIT license: https://codemirror.net/LICENSE
4 // declare global: DOMRect
7 if (typeof exports
== "object" && typeof module
== "object") // CommonJS
8 mod(require("../../lib/codemirror"));
9 else if (typeof define
== "function" && define
.amd
) // AMD
10 define(["../../lib/codemirror"], mod
);
11 else // Plain browser env
13 })(function(CodeMirror
) {
16 var HINT_ELEMENT_CLASS
= "CodeMirror-hint";
17 var ACTIVE_HINT_ELEMENT_CLASS
= "CodeMirror-hint-active";
19 // This is the old interface, kept around for now to stay
20 // backwards-compatible.
21 CodeMirror
.showHint = function(cm
, getHints
, options
) {
22 if (!getHints
) return cm
.showHint(options
);
23 if (options
&& options
.async
) getHints
.async
= true;
24 var newOpts
= {hint
: getHints
};
25 if (options
) for (var prop
in options
) newOpts
[prop
] = options
[prop
];
26 return cm
.showHint(newOpts
);
29 CodeMirror
.defineExtension("showHint", function(options
) {
30 options
= parseOptions(this, this.getCursor("start"), options
);
31 var selections
= this.listSelections()
32 if (selections
.length
> 1) return;
33 // By default, don't allow completion when something is selected.
34 // A hint function can have a `supportsSelection` property to
35 // indicate that it can handle selections.
36 if (this.somethingSelected()) {
37 if (!options
.hint
.supportsSelection
) return;
38 // Don't try with cross-line selections
39 for (var i
= 0; i
< selections
.length
; i
++)
40 if (selections
[i
].head
.line
!= selections
[i
].anchor
.line
) return;
43 if (this.state
.completionActive
) this.state
.completionActive
.close();
44 var completion
= this.state
.completionActive
= new Completion(this, options
);
45 if (!completion
.options
.hint
) return;
47 CodeMirror
.signal(this, "startCompletion", this);
48 completion
.update(true);
51 CodeMirror
.defineExtension("closeHint", function() {
52 if (this.state
.completionActive
) this.state
.completionActive
.close()
55 function Completion(cm
, options
) {
57 this.options
= options
;
61 this.startPos
= this.cm
.getCursor("start");
62 this.startLen
= this.cm
.getLine(this.startPos
.line
).length
- this.cm
.getSelection().length
;
64 if (this.options
.updateOnCursorActivity
) {
66 cm
.on("cursorActivity", this.activityFunc = function() { self
.cursorActivity(); });
70 var requestAnimationFrame
= window
.requestAnimationFrame
|| function(fn
) {
71 return setTimeout(fn
, 1000/60);
73 var cancelAnimationFrame
= window
.cancelAnimationFrame
|| clearTimeout
;
75 Completion
.prototype = {
77 if (!this.active()) return;
78 this.cm
.state
.completionActive
= null;
80 if (this.options
.updateOnCursorActivity
) {
81 this.cm
.off("cursorActivity", this.activityFunc
);
84 if (this.widget
&& this.data
) CodeMirror
.signal(this.data
, "close");
85 if (this.widget
) this.widget
.close();
86 CodeMirror
.signal(this.cm
, "endCompletion", this.cm
);
90 return this.cm
.state
.completionActive
== this;
93 pick: function(data
, i
) {
94 var completion
= data
.list
[i
], self
= this;
95 this.cm
.operation(function() {
97 completion
.hint(self
.cm
, data
, completion
);
99 self
.cm
.replaceRange(getText(completion
), completion
.from || data
.from,
100 completion
.to
|| data
.to
, "complete");
101 CodeMirror
.signal(data
, "pick", completion
);
102 self
.cm
.scrollIntoView();
104 if (this.options
.closeOnPick
) {
109 cursorActivity: function() {
111 cancelAnimationFrame(this.debounce
);
115 var identStart
= this.startPos
;
117 identStart
= this.data
.from;
120 var pos
= this.cm
.getCursor(), line
= this.cm
.getLine(pos
.line
);
121 if (pos
.line
!= this.startPos
.line
|| line
.length
- pos
.ch
!= this.startLen
- this.startPos
.ch
||
122 pos
.ch
< identStart
.ch
|| this.cm
.somethingSelected() ||
123 (!pos
.ch
|| this.options
.closeCharacters
.test(line
.charAt(pos
.ch
- 1)))) {
127 this.debounce
= requestAnimationFrame(function() {self
.update();});
128 if (this.widget
) this.widget
.disable();
132 update: function(first
) {
133 if (this.tick
== null) return
134 var self
= this, myTick
= ++this.tick
135 fetchHints(this.options
.hint
, this.cm
, this.options
, function(data
) {
136 if (self
.tick
== myTick
) self
.finishUpdate(data
, first
)
140 finishUpdate: function(data
, first
) {
141 if (this.data
) CodeMirror
.signal(this.data
, "update");
143 var picked
= (this.widget
&& this.widget
.picked
) || (first
&& this.options
.completeSingle
);
144 if (this.widget
) this.widget
.close();
148 if (data
&& data
.list
.length
) {
149 if (picked
&& data
.list
.length
== 1) {
152 this.widget
= new Widget(this, data
);
153 CodeMirror
.signal(data
, "shown");
159 function parseOptions(cm
, pos
, options
) {
160 var editor
= cm
.options
.hintOptions
;
162 for (var prop
in defaultOptions
) out
[prop
] = defaultOptions
[prop
];
163 if (editor
) for (var prop
in editor
)
164 if (editor
[prop
] !== undefined) out
[prop
] = editor
[prop
];
165 if (options
) for (var prop
in options
)
166 if (options
[prop
] !== undefined) out
[prop
] = options
[prop
];
167 if (out
.hint
.resolve
) out
.hint
= out
.hint
.resolve(cm
, pos
)
171 function getText(completion
) {
172 if (typeof completion
== "string") return completion
;
173 else return completion
.text
;
176 function buildKeyMap(completion
, handle
) {
178 Up: function() {handle
.moveFocus(-1);},
179 Down: function() {handle
.moveFocus(1);},
180 PageUp: function() {handle
.moveFocus(-handle
.menuSize() + 1, true);},
181 PageDown: function() {handle
.moveFocus(handle
.menuSize() - 1, true);},
182 Home: function() {handle
.setFocus(0);},
183 End: function() {handle
.setFocus(handle
.length
- 1);},
189 var mac
= /Mac/.test(navigator
.platform
);
192 baseMap
["Ctrl-P"] = function() {handle
.moveFocus(-1);};
193 baseMap
["Ctrl-N"] = function() {handle
.moveFocus(1);};
196 var custom
= completion
.options
.customKeys
;
197 var ourMap
= custom
? {} : baseMap
;
198 function addBinding(key
, val
) {
200 if (typeof val
!= "string")
201 bound = function(cm
) { return val(cm
, handle
); };
202 // This mechanism is deprecated
203 else if (baseMap
.hasOwnProperty(val
))
204 bound
= baseMap
[val
];
210 for (var key
in custom
) if (custom
.hasOwnProperty(key
))
211 addBinding(key
, custom
[key
]);
212 var extra
= completion
.options
.extraKeys
;
214 for (var key
in extra
) if (extra
.hasOwnProperty(key
))
215 addBinding(key
, extra
[key
]);
219 function getHintElement(hintsElement
, el
) {
220 while (el
&& el
!= hintsElement
) {
221 if (el
.nodeName
.toUpperCase() === "LI" && el
.parentNode
== hintsElement
) return el
;
226 function Widget(completion
, data
) {
227 this.completion
= completion
;
230 var widget
= this, cm
= completion
.cm
;
231 var ownerDocument
= cm
.getInputField().ownerDocument
;
232 var parentWindow
= ownerDocument
.defaultView
|| ownerDocument
.parentWindow
;
234 var hints
= this.hints
= ownerDocument
.createElement("ul");
235 var theme
= completion
.cm
.options
.theme
;
236 hints
.className
= "CodeMirror-hints " + theme
;
237 this.selectedHint
= data
.selectedHint
|| 0;
239 var completions
= data
.list
;
240 for (var i
= 0; i
< completions
.length
; ++i
) {
241 var elt
= hints
.appendChild(ownerDocument
.createElement("li")), cur
= completions
[i
];
242 var className
= HINT_ELEMENT_CLASS
+ (i
!= this.selectedHint
? "" : " " + ACTIVE_HINT_ELEMENT_CLASS
);
243 if (cur
.className
!= null) className
= cur
.className
+ " " + className
;
244 elt
.className
= className
;
245 if (cur
.render
) cur
.render(elt
, data
, cur
);
246 else elt
.appendChild(ownerDocument
.createTextNode(cur
.displayText
|| getText(cur
)));
250 var container
= completion
.options
.container
|| ownerDocument
.body
;
251 var pos
= cm
.cursorCoords(completion
.options
.alignWithWord
? data
.from : null);
252 var left
= pos
.left
, top
= pos
.bottom
, below
= true;
253 var offsetLeft
= 0, offsetTop
= 0;
254 if (container
!== ownerDocument
.body
) {
255 // We offset the cursor position because left and top are relative to the offsetParent's top left corner.
256 var isContainerPositioned
= ['absolute', 'relative', 'fixed'].indexOf(parentWindow
.getComputedStyle(container
).position
) !== -1;
257 var offsetParent
= isContainerPositioned
? container
: container
.offsetParent
;
258 var offsetParentPosition
= offsetParent
.getBoundingClientRect();
259 var bodyPosition
= ownerDocument
.body
.getBoundingClientRect();
260 offsetLeft
= (offsetParentPosition
.left
- bodyPosition
.left
- offsetParent
.scrollLeft
);
261 offsetTop
= (offsetParentPosition
.top
- bodyPosition
.top
- offsetParent
.scrollTop
);
263 hints
.style
.left
= (left
- offsetLeft
) + "px";
264 hints
.style
.top
= (top
- offsetTop
) + "px";
266 // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
267 var winW
= parentWindow
.innerWidth
|| Math
.max(ownerDocument
.body
.offsetWidth
, ownerDocument
.documentElement
.offsetWidth
);
268 var winH
= parentWindow
.innerHeight
|| Math
.max(ownerDocument
.body
.offsetHeight
, ownerDocument
.documentElement
.offsetHeight
);
269 container
.appendChild(hints
);
271 var box
= completion
.options
.moveOnOverlap
? hints
.getBoundingClientRect() : new DOMRect();
272 var scrolls
= completion
.options
.paddingForScrollbar
? hints
.scrollHeight
> hints
.clientHeight
+ 1 : false;
274 // Compute in the timeout to avoid reflow on init
276 setTimeout(function() { startScroll
= cm
.getScrollInfo(); });
278 var overlapY
= box
.bottom
- winH
;
280 var height
= box
.bottom
- box
.top
, curTop
= pos
.top
- (pos
.bottom
- box
.top
);
281 if (curTop
- height
> 0) { // Fits above cursor
282 hints
.style
.top
= (top
= pos
.top
- height
- offsetTop
) + "px";
284 } else if (height
> winH
) {
285 hints
.style
.height
= (winH
- 5) + "px";
286 hints
.style
.top
= (top
= pos
.bottom
- box
.top
- offsetTop
) + "px";
287 var cursor
= cm
.getCursor();
288 if (data
.from.ch
!= cursor
.ch
) {
289 pos
= cm
.cursorCoords(cursor
);
290 hints
.style
.left
= (left
= pos
.left
- offsetLeft
) + "px";
291 box
= hints
.getBoundingClientRect();
295 var overlapX
= box
.right
- winW
;
296 if (scrolls
) overlapX
+= cm
.display
.nativeBarWidth
;
298 if (box
.right
- box
.left
> winW
) {
299 hints
.style
.width
= (winW
- 5) + "px";
300 overlapX
-= (box
.right
- box
.left
) - winW
;
302 hints
.style
.left
= (left
= pos
.left
- overlapX
- offsetLeft
) + "px";
304 if (scrolls
) for (var node
= hints
.firstChild
; node
; node
= node
.nextSibling
)
305 node
.style
.paddingRight
= cm
.display
.nativeBarWidth
+ "px"
307 cm
.addKeyMap(this.keyMap
= buildKeyMap(completion
, {
308 moveFocus: function(n
, avoidWrap
) { widget
.changeActive(widget
.selectedHint
+ n
, avoidWrap
); },
309 setFocus: function(n
) { widget
.changeActive(n
); },
310 menuSize: function() { return widget
.screenAmount(); },
311 length
: completions
.length
,
312 close: function() { completion
.close(); },
313 pick: function() { widget
.pick(); },
317 if (completion
.options
.closeOnUnfocus
) {
319 cm
.on("blur", this.onBlur = function() { closingOnBlur
= setTimeout(function() { completion
.close(); }, 100); });
320 cm
.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur
); });
323 cm
.on("scroll", this.onScroll = function() {
324 var curScroll
= cm
.getScrollInfo(), editor
= cm
.getWrapperElement().getBoundingClientRect();
325 if (!startScroll
) startScroll
= cm
.getScrollInfo();
326 var newTop
= top
+ startScroll
.top
- curScroll
.top
;
327 var point
= newTop
- (parentWindow
.pageYOffset
|| (ownerDocument
.documentElement
|| ownerDocument
.body
).scrollTop
);
328 if (!below
) point
+= hints
.offsetHeight
;
329 if (point
<= editor
.top
|| point
>= editor
.bottom
) return completion
.close();
330 hints
.style
.top
= newTop
+ "px";
331 hints
.style
.left
= (left
+ startScroll
.left
- curScroll
.left
) + "px";
334 CodeMirror
.on(hints
, "dblclick", function(e
) {
335 var t
= getHintElement(hints
, e
.target
|| e
.srcElement
);
336 if (t
&& t
.hintId
!= null) {widget
.changeActive(t
.hintId
); widget
.pick();}
339 CodeMirror
.on(hints
, "click", function(e
) {
340 var t
= getHintElement(hints
, e
.target
|| e
.srcElement
);
341 if (t
&& t
.hintId
!= null) {
342 widget
.changeActive(t
.hintId
);
343 if (completion
.options
.completeOnSingleClick
) widget
.pick();
347 CodeMirror
.on(hints
, "mousedown", function() {
348 setTimeout(function(){cm
.focus();}, 20);
351 // The first hint doesn't need to be scrolled to on init
352 var selectedHintRange
= this.getSelectedHintRange();
353 if (selectedHintRange
.from !== 0 || selectedHintRange
.to
!== 0) {
354 this.scrollToActive();
357 CodeMirror
.signal(data
, "select", completions
[this.selectedHint
], hints
.childNodes
[this.selectedHint
]);
363 if (this.completion
.widget
!= this) return;
364 this.completion
.widget
= null;
365 if (this.hints
.parentNode
) this.hints
.parentNode
.removeChild(this.hints
);
366 this.completion
.cm
.removeKeyMap(this.keyMap
);
368 var cm
= this.completion
.cm
;
369 if (this.completion
.options
.closeOnUnfocus
) {
370 cm
.off("blur", this.onBlur
);
371 cm
.off("focus", this.onFocus
);
373 cm
.off("scroll", this.onScroll
);
376 disable: function() {
377 this.completion
.cm
.removeKeyMap(this.keyMap
);
379 this.keyMap
= {Enter: function() { widget
.picked
= true; }};
380 this.completion
.cm
.addKeyMap(this.keyMap
);
384 this.completion
.pick(this.data
, this.selectedHint
);
387 changeActive: function(i
, avoidWrap
) {
388 if (i
>= this.data
.list
.length
)
389 i
= avoidWrap
? this.data
.list
.length
- 1 : 0;
391 i
= avoidWrap
? 0 : this.data
.list
.length
- 1;
392 if (this.selectedHint
== i
) return;
393 var node
= this.hints
.childNodes
[this.selectedHint
];
394 if (node
) node
.className
= node
.className
.replace(" " + ACTIVE_HINT_ELEMENT_CLASS
, "");
395 node
= this.hints
.childNodes
[this.selectedHint
= i
];
396 node
.className
+= " " + ACTIVE_HINT_ELEMENT_CLASS
;
397 this.scrollToActive()
398 CodeMirror
.signal(this.data
, "select", this.data
.list
[this.selectedHint
], node
);
401 scrollToActive: function() {
402 var selectedHintRange
= this.getSelectedHintRange();
403 var node1
= this.hints
.childNodes
[selectedHintRange
.from];
404 var node2
= this.hints
.childNodes
[selectedHintRange
.to
];
405 var firstNode
= this.hints
.firstChild
;
406 if (node1
.offsetTop
< this.hints
.scrollTop
)
407 this.hints
.scrollTop
= node1
.offsetTop
- firstNode
.offsetTop
;
408 else if (node2
.offsetTop
+ node2
.offsetHeight
> this.hints
.scrollTop
+ this.hints
.clientHeight
)
409 this.hints
.scrollTop
= node2
.offsetTop
+ node2
.offsetHeight
- this.hints
.clientHeight
+ firstNode
.offsetTop
;
412 screenAmount: function() {
413 return Math
.floor(this.hints
.clientHeight
/ this.hints
.firstChild
.offsetHeight
) || 1;
416 getSelectedHintRange: function() {
417 var margin
= this.completion
.options
.scrollMargin
|| 0;
419 from: Math
.max(0, this.selectedHint
- margin
),
420 to
: Math
.min(this.data
.list
.length
- 1, this.selectedHint
+ margin
),
425 function applicableHelpers(cm
, helpers
) {
426 if (!cm
.somethingSelected()) return helpers
428 for (var i
= 0; i
< helpers
.length
; i
++)
429 if (helpers
[i
].supportsSelection
) result
.push(helpers
[i
])
433 function fetchHints(hint
, cm
, options
, callback
) {
435 hint(cm
, callback
, options
)
437 var result
= hint(cm
, options
)
438 if (result
&& result
.then
) result
.then(callback
)
439 else callback(result
)
443 function resolveAutoHints(cm
, pos
) {
444 var helpers
= cm
.getHelpers(pos
, "hint"), words
445 if (helpers
.length
) {
446 var resolved = function(cm
, callback
, options
) {
447 var app
= applicableHelpers(cm
, helpers
);
449 if (i
== app
.length
) return callback(null)
450 fetchHints(app
[i
], cm
, options
, function(result
) {
451 if (result
&& result
.list
.length
> 0) callback(result
)
457 resolved
.async
= true
458 resolved
.supportsSelection
= true
460 } else if (words
= cm
.getHelper(cm
.getCursor(), "hintWords")) {
461 return function(cm
) { return CodeMirror
.hint
.fromList(cm
, {words
: words
}) }
462 } else if (CodeMirror
.hint
.anyword
) {
463 return function(cm
, options
) { return CodeMirror
.hint
.anyword(cm
, options
) }
469 CodeMirror
.registerHelper("hint", "auto", {
470 resolve
: resolveAutoHints
473 CodeMirror
.registerHelper("hint", "fromList", function(cm
, options
) {
474 var cur
= cm
.getCursor(), token
= cm
.getTokenAt(cur
)
475 var term
, from = CodeMirror
.Pos(cur
.line
, token
.start
), to
= cur
476 if (token
.start
< cur
.ch
&& /\w/.test(token
.string
.charAt(cur
.ch
- token
.start
- 1))) {
477 term
= token
.string
.substr(0, cur
.ch
- token
.start
)
483 for (var i
= 0; i
< options
.words
.length
; i
++) {
484 var word
= options
.words
[i
];
485 if (word
.slice(0, term
.length
) == term
)
489 if (found
.length
) return {list
: found
, from: from, to
: to
};
492 CodeMirror
.commands
.autocomplete
= CodeMirror
.showHint
;
494 var defaultOptions
= {
495 hint
: CodeMirror
.hint
.auto
,
496 completeSingle
: true,
498 closeCharacters
: /[\s()\[\]{};:>,]/,
500 closeOnUnfocus
: true,
501 updateOnCursorActivity
: true,
502 completeOnSingleClick
: true,
506 paddingForScrollbar
: true,
510 CodeMirror
.defineOption("hintOptions", null);