1 // Glue code between CodeMirror and Tern.
3 // Create a CodeMirror.TernServer to wrap an actual Tern server,
4 // register open documents (CodeMirror.Doc instances) with it, and
5 // call its methods to activate the assisting functions that Tern
8 // Options supported (all optional):
9 // * defs: An array of JSON definition data structures.
10 // * plugins: An object mapping plugin names to configuration
12 // * getFile: A function(name, c) that can be used to access files in
13 // the project that haven't been loaded yet. Simply do c(null) to
14 // indicate that a file is not available.
15 // * fileFilter: A function(value, docName, doc) that will be applied
16 // to documents before passing them on to Tern.
17 // * switchToDoc: A function(name) that should, when providing a
18 // multi-file view, switch the view or focus to the named file.
19 // * showError: A function(editor, message) that can be used to
20 // override the way errors are displayed.
21 // * completionTip: Customize the content in tooltips for completions.
22 // Is passed a single argument—the completion's data as returned by
23 // Tern—and may return a string, DOM node, or null to indicate that
24 // no tip should be shown. By default the docstring is shown.
25 // * typeTip: Like completionTip, but for the tooltips shown for type
27 // * responseFilter: A function(doc, query, request, error, data) that
28 // will be applied to the Tern responses before treating them
31 // It is possible to run the Tern server in a web worker by specifying
32 // these additional options:
33 // * useWorker: Set to true to enable web worker mode. You'll probably
34 // want to feature detect the actual value you use here, for example
36 // * workerScript: The main script of the worker. Point this to
37 // wherever you are hosting worker.js from this directory.
38 // * workerDeps: An array of paths pointing (relative to workerScript)
39 // to the Acorn and Tern libraries and any Tern plugins you want to
40 // load. Or, if you minified those into a single script and included
41 // them in the workerScript, simply leave this undefined.
44 if (typeof exports
== "object" && typeof module
== "object") // CommonJS
45 mod(require("../../lib/codemirror"));
46 else if (typeof define
== "function" && define
.amd
) // AMD
47 define(["../../lib/codemirror"], mod
);
48 else // Plain browser env
50 })(function(CodeMirror
) {
52 // declare global: tern
54 CodeMirror
.TernServer = function(options
) {
56 this.options
= options
|| {};
57 var plugins
= this.options
.plugins
|| (this.options
.plugins
= {});
58 if (!plugins
.doc_comment
) plugins
.doc_comment
= true;
59 if (this.options
.useWorker
) {
60 this.server
= new WorkerServer(this);
62 this.server
= new tern
.Server({
63 getFile: function(name
, c
) { return getFile(self
, name
, c
); },
65 defs
: this.options
.defs
|| [],
69 this.docs
= Object
.create(null);
70 this.trackChange = function(doc
, change
) { trackChange(self
, doc
, change
); };
72 this.cachedArgHints
= null;
73 this.activeArgHints
= null;
77 CodeMirror
.TernServer
.prototype = {
78 addDoc: function(name
, doc
) {
79 var data
= {doc
: doc
, name
: name
, changed
: null};
80 this.server
.addFile(name
, docValue(this, data
));
81 CodeMirror
.on(doc
, "change", this.trackChange
);
82 return this.docs
[name
] = data
;
85 delDoc: function(name
) {
86 var found
= this.docs
[name
];
88 CodeMirror
.off(found
.doc
, "change", this.trackChange
);
89 delete this.docs
[name
];
90 this.server
.delFile(name
);
93 hideDoc: function(name
) {
95 var found
= this.docs
[name
];
96 if (found
&& found
.changed
) sendDoc(this, found
);
99 complete: function(cm
) {
101 CodeMirror
.showHint(cm
, function(cm
, c
) { return hint(self
, cm
, c
); }, {async
: true});
104 getHint: function(cm
, c
) { return hint(this, cm
, c
); },
106 showType: function(cm
, pos
) { showType(this, cm
, pos
); },
108 updateArgHints: function(cm
) { updateArgHints(this, cm
); },
110 jumpToDef: function(cm
) { jumpToDef(this, cm
); },
112 jumpBack: function(cm
) { jumpBack(this, cm
); },
114 rename: function(cm
) { rename(this, cm
); },
116 selectName: function(cm
) { selectName(this, cm
); },
118 request: function (cm
, query
, c
, pos
) {
120 var doc
= findDoc(this, cm
.getDoc());
121 var request
= buildRequest(this, doc
, query
, pos
);
123 this.server
.request(request
, function (error
, data
) {
124 if (!error
&& self
.options
.responseFilter
)
125 data
= self
.options
.responseFilter(doc
, query
, request
, error
, data
);
131 var Pos
= CodeMirror
.Pos
;
132 var cls
= "CodeMirror-Tern-";
135 function getFile(ts
, name
, c
) {
136 var buf
= ts
.docs
[name
];
138 c(docValue(ts
, buf
));
139 else if (ts
.options
.getFile
)
140 ts
.options
.getFile(name
, c
);
145 function findDoc(ts
, doc
, name
) {
146 for (var n
in ts
.docs
) {
147 var cur
= ts
.docs
[n
];
148 if (cur
.doc
== doc
) return cur
;
150 if (!name
) for (var i
= 0;; ++i
) {
151 n
= "[doc" + (i
|| "") + "]";
152 if (!ts
.docs
[n
]) { name
= n
; break; }
154 return ts
.addDoc(name
, doc
);
157 function trackChange(ts
, doc
, change
) {
158 var data
= findDoc(ts
, doc
);
160 var argHints
= ts
.cachedArgHints
;
161 if (argHints
&& argHints
.doc
== doc
&& cmpPos(argHints
.start
, change
.to
) <= 0)
162 ts
.cachedArgHints
= null;
164 var changed
= data
.changed
;
166 data
.changed
= changed
= {from: change
.from.line
, to
: change
.from.line
};
167 var end
= change
.from.line
+ (change
.text
.length
- 1);
168 if (change
.from.line
< changed
.to
) changed
.to
= changed
.to
- (change
.to
.line
- end
);
169 if (end
>= changed
.to
) changed
.to
= end
+ 1;
170 if (changed
.from > change
.from.line
) changed
.from = change
.from.line
;
172 if (doc
.lineCount() > bigDoc
&& change
.to
- changed
.from > 100) setTimeout(function() {
173 if (data
.changed
&& data
.changed
.to
- data
.changed
.from > 100) sendDoc(ts
, data
);
177 function sendDoc(ts
, doc
) {
178 ts
.server
.request({files
: [{type
: "full", name
: doc
.name
, text
: docValue(ts
, doc
)}]}, function(error
) {
179 if (error
) window
.console
.error(error
);
180 else doc
.changed
= null;
186 function hint(ts
, cm
, c
) {
187 ts
.request(cm
, {type
: "completions", types
: true, docs
: true, urls
: true}, function(error
, data
) {
188 if (error
) return showError(ts
, cm
, error
);
189 var completions
= [], after
= "";
190 var from = data
.start
, to
= data
.end
;
191 if (cm
.getRange(Pos(from.line
, from.ch
- 2), from) == "[\"" &&
192 cm
.getRange(to
, Pos(to
.line
, to
.ch
+ 2)) != "\"]")
195 for (var i
= 0; i
< data
.completions
.length
; ++i
) {
196 var completion
= data
.completions
[i
], className
= typeToIcon(completion
.type
);
197 if (data
.guess
) className
+= " " + cls
+ "guess";
198 completions
.push({text
: completion
.name
+ after
,
199 displayText
: completion
.name
,
200 className
: className
,
204 var obj
= {from: from, to
: to
, list
: completions
};
206 CodeMirror
.on(obj
, "close", function() { remove(tooltip
); });
207 CodeMirror
.on(obj
, "update", function() { remove(tooltip
); });
208 CodeMirror
.on(obj
, "select", function(cur
, node
) {
210 var content
= ts
.options
.completionTip
? ts
.options
.completionTip(cur
.data
) : cur
.data
.doc
;
212 tooltip
= makeTooltip(node
.parentNode
.getBoundingClientRect().right
+ window
.pageXOffset
,
213 node
.getBoundingClientRect().top
+ window
.pageYOffset
, content
);
214 tooltip
.className
+= " " + cls
+ "hint-doc";
221 function typeToIcon(type
) {
223 if (type
== "?") suffix
= "unknown";
224 else if (type
== "number" || type
== "string" || type
== "bool") suffix
= type
;
225 else if (/^fn\(/.test(type
)) suffix
= "fn";
226 else if (/^\[/.test(type
)) suffix
= "array";
227 else suffix
= "object";
228 return cls
+ "completion " + cls
+ "completion-" + suffix
;
233 function showType(ts
, cm
, pos
) {
234 ts
.request(cm
, "type", function(error
, data
) {
235 if (error
) return showError(ts
, cm
, error
);
236 if (ts
.options
.typeTip
) {
237 var tip
= ts
.options
.typeTip(data
);
239 var tip
= elt("span", null, elt("strong", null, data
.type
|| "not found"));
241 tip
.appendChild(document
.createTextNode(" — " + data
.doc
));
243 tip
.appendChild(document
.createTextNode(" "));
244 tip
.appendChild(elt("a", null, "[docs]")).href
= data
.url
;
247 tempTooltip(cm
, tip
);
251 // Maintaining argument hints
253 function updateArgHints(ts
, cm
) {
256 if (cm
.somethingSelected()) return;
257 var state
= cm
.getTokenAt(cm
.getCursor()).state
;
258 var inner
= CodeMirror
.innerMode(cm
.getMode(), state
);
259 if (inner
.mode
.name
!= "javascript") return;
260 var lex
= inner
.state
.lexical
;
261 if (lex
.info
!= "call") return;
263 var ch
, argPos
= lex
.pos
|| 0, tabSize
= cm
.getOption("tabSize");
264 for (var line
= cm
.getCursor().line
, e
= Math
.max(0, line
- 9), found
= false; line
>= e
; --line
) {
265 var str
= cm
.getLine(line
), extra
= 0;
266 for (var pos
= 0;;) {
267 var tab
= str
.indexOf("\t", pos
);
268 if (tab
== -1) break;
269 extra
+= tabSize
- (tab
+ extra
) % tabSize
- 1;
272 ch
= lex
.column
- extra
;
273 if (str
.charAt(ch
) == "(") {found
= true; break;}
277 var start
= Pos(line
, ch
);
278 var cache
= ts
.cachedArgHints
;
279 if (cache
&& cache
.doc
== cm
.getDoc() && cmpPos(start
, cache
.start
) == 0)
280 return showArgHints(ts
, cm
, argPos
);
282 ts
.request(cm
, {type
: "type", preferFunction
: true, end
: start
}, function(error
, data
) {
283 if (error
|| !data
.type
|| !(/^fn\(/).test(data
.type
)) return;
284 ts
.cachedArgHints
= {
286 type
: parseFnType(data
.type
),
287 name
: data
.exprName
|| data
.name
|| "fn",
291 showArgHints(ts
, cm
, argPos
);
295 function showArgHints(ts
, cm
, pos
) {
298 var cache
= ts
.cachedArgHints
, tp
= cache
.type
;
299 var tip
= elt("span", cache
.guess
? cls
+ "fhint-guess" : null,
300 elt("span", cls
+ "fname", cache
.name
), "(");
301 for (var i
= 0; i
< tp
.args
.length
; ++i
) {
302 if (i
) tip
.appendChild(document
.createTextNode(", "));
303 var arg
= tp
.args
[i
];
304 tip
.appendChild(elt("span", cls
+ "farg" + (i
== pos
? " " + cls
+ "farg-current" : ""), arg
.name
|| "?"));
305 if (arg
.type
!= "?") {
306 tip
.appendChild(document
.createTextNode(":\u00a0"));
307 tip
.appendChild(elt("span", cls
+ "type", arg
.type
));
310 tip
.appendChild(document
.createTextNode(tp
.rettype
? ") ->\u00a0" : ")"));
311 if (tp
.rettype
) tip
.appendChild(elt("span", cls
+ "type", tp
.rettype
));
312 var place
= cm
.cursorCoords(null, "page");
313 ts
.activeArgHints
= makeTooltip(place
.right
+ 1, place
.bottom
, tip
);
316 function parseFnType(text
) {
317 var args
= [], pos
= 3;
319 function skipMatching(upto
) {
320 var depth
= 0, start
= pos
;
322 var next
= text
.charAt(pos
);
323 if (upto
.test(next
) && !depth
) return text
.slice(start
, pos
);
324 if (/[{\[\(]/.test(next
)) ++depth
;
325 else if (/[}\]\)]/.test(next
)) --depth
;
331 if (text
.charAt(pos
) != ")") for (;;) {
332 var name
= text
.slice(pos
).match(/^([^, \(\[\{]+): /);
334 pos
+= name
[0].length
;
337 args
.push({name
: name
, type
: skipMatching(/[\),]/)});
338 if (text
.charAt(pos
) == ")") break;
342 var rettype
= text
.slice(pos
).match(/^\) -> (.*)$/);
344 return {args
: args
, rettype
: rettype
&& rettype
[1]};
347 // Moving to the definition of something
349 function jumpToDef(ts
, cm
) {
350 function inner(varName
) {
351 var req
= {type
: "definition", variable
: varName
|| null};
352 var doc
= findDoc(ts
, cm
.getDoc());
353 ts
.server
.request(buildRequest(ts
, doc
, req
), function(error
, data
) {
354 if (error
) return showError(ts
, cm
, error
);
355 if (!data
.file
&& data
.url
) { window
.open(data
.url
); return; }
358 var localDoc
= ts
.docs
[data
.file
], found
;
359 if (localDoc
&& (found
= findContext(localDoc
.doc
, data
))) {
360 ts
.jumpStack
.push({file
: doc
.name
,
361 start
: cm
.getCursor("from"),
362 end
: cm
.getCursor("to")});
363 moveTo(ts
, doc
, localDoc
, found
.start
, found
.end
);
367 showError(ts
, cm
, "Could not find a definition.");
371 if (!atInterestingExpression(cm
))
372 dialog(cm
, "Jump to variable", function(name
) { if (name
) inner(name
); });
377 function jumpBack(ts
, cm
) {
378 var pos
= ts
.jumpStack
.pop(), doc
= pos
&& ts
.docs
[pos
.file
];
380 moveTo(ts
, findDoc(ts
, cm
.getDoc()), doc
, pos
.start
, pos
.end
);
383 function moveTo(ts
, curDoc
, doc
, start
, end
) {
384 doc
.doc
.setSelection(end
, start
);
385 if (curDoc
!= doc
&& ts
.options
.switchToDoc
) {
387 ts
.options
.switchToDoc(doc
.name
);
391 // The {line,ch} representation of positions makes this rather awkward.
392 function findContext(doc
, data
) {
393 var before
= data
.context
.slice(0, data
.contextOffset
).split("\n");
394 var startLine
= data
.start
.line
- (before
.length
- 1);
395 var start
= Pos(startLine
, (before
.length
== 1 ? data
.start
.ch
: doc
.getLine(startLine
).length
) - before
[0].length
);
397 var text
= doc
.getLine(startLine
).slice(start
.ch
);
398 for (var cur
= startLine
+ 1; cur
< doc
.lineCount() && text
.length
< data
.context
.length
; ++cur
)
399 text
+= "\n" + doc
.getLine(cur
);
400 if (text
.slice(0, data
.context
.length
) == data
.context
) return data
;
402 var cursor
= doc
.getSearchCursor(data
.context
, 0, false);
403 var nearest
, nearestDist
= Infinity
;
404 while (cursor
.findNext()) {
405 var from = cursor
.from(), dist
= Math
.abs(from.line
- start
.line
) * 10000;
406 if (!dist
) dist
= Math
.abs(from.ch
- start
.ch
);
407 if (dist
< nearestDist
) { nearest
= from; nearestDist
= dist
; }
409 if (!nearest
) return null;
411 if (before
.length
== 1)
412 nearest
.ch
+= before
[0].length
;
414 nearest
= Pos(nearest
.line
+ (before
.length
- 1), before
[before
.length
- 1].length
);
415 if (data
.start
.line
== data
.end
.line
)
416 var end
= Pos(nearest
.line
, nearest
.ch
+ (data
.end
.ch
- data
.start
.ch
));
418 var end
= Pos(nearest
.line
+ (data
.end
.line
- data
.start
.line
), data
.end
.ch
);
419 return {start
: nearest
, end
: end
};
422 function atInterestingExpression(cm
) {
423 var pos
= cm
.getCursor("end"), tok
= cm
.getTokenAt(pos
);
424 if (tok
.start
< pos
.ch
&& (tok
.type
== "comment" || tok
.type
== "string")) return false;
425 return /\w/.test(cm
.getLine(pos
.line
).slice(Math
.max(pos
.ch
- 1, 0), pos
.ch
+ 1));
430 function rename(ts
, cm
) {
431 var token
= cm
.getTokenAt(cm
.getCursor());
432 if (!/\w/.test(token
.string
)) showError(ts
, cm
, "Not at a variable");
433 dialog(cm
, "New name for " + token
.string
, function(newName
) {
434 ts
.request(cm
, {type
: "rename", newName
: newName
, fullDocs
: true}, function(error
, data
) {
435 if (error
) return showError(ts
, cm
, error
);
436 applyChanges(ts
, data
.changes
);
441 function selectName(ts
, cm
) {
442 var cur
= cm
.getCursor(), token
= cm
.getTokenAt(cur
);
443 if (!/\w/.test(token
.string
)) showError(ts
, cm
, "Not at a variable");
444 var name
= findDoc(ts
, cm
.doc
).name
;
445 ts
.request(cm
, {type
: "refs"}, function(error
, data
) {
446 if (error
) return showError(ts
, cm
, error
);
447 var ranges
= [], cur
= 0;
448 for (var i
= 0; i
< data
.refs
.length
; i
++) {
449 var ref
= data
.refs
[i
];
450 if (ref
.file
== name
) {
451 ranges
.push({anchor
: ref
.start
, head
: ref
.end
});
452 if (cmpPos(cur
, ref
.start
) >= 0 && cmpPos(cur
, ref
.end
) <= 0)
453 cur
= ranges
.length
- 1;
456 cm
.setSelections(ranges
, cur
);
460 var nextChangeOrig
= 0;
461 function applyChanges(ts
, changes
) {
462 var perFile
= Object
.create(null);
463 for (var i
= 0; i
< changes
.length
; ++i
) {
465 (perFile
[ch
.file
] || (perFile
[ch
.file
] = [])).push(ch
);
467 for (var file
in perFile
) {
468 var known
= ts
.docs
[file
], chs
= perFile
[file
];;
469 if (!known
) continue;
470 chs
.sort(function(a
, b
) { return cmpPos(b
.start
, a
.start
); });
471 var origin
= "*rename" + (++nextChangeOrig
);
472 for (var i
= 0; i
< chs
.length
; ++i
) {
474 known
.doc
.replaceRange(ch
.text
, ch
.start
, ch
.end
, origin
);
479 // Generic request-building helper
481 function buildRequest(ts
, doc
, query
, pos
) {
482 var files
= [], offsetLines
= 0, allowFragments
= !query
.fullDocs
;
483 if (!allowFragments
) delete query
.fullDocs
;
484 if (typeof query
== "string") query
= {type
: query
};
485 query
.lineCharPositions
= true;
486 if (query
.end
== null) {
487 query
.end
= pos
|| doc
.doc
.getCursor("end");
488 if (doc
.doc
.somethingSelected())
489 query
.start
= doc
.doc
.getCursor("start");
491 var startPos
= query
.start
|| query
.end
;
494 if (doc
.doc
.lineCount() > bigDoc
&& allowFragments
!== false &&
495 doc
.changed
.to
- doc
.changed
.from < 100 &&
496 doc
.changed
.from <= startPos
.line
&& doc
.changed
.to
> query
.end
.line
) {
497 files
.push(getFragmentAround(doc
, startPos
, query
.end
));
499 var offsetLines
= files
[0].offsetLines
;
500 if (query
.start
!= null) query
.start
= Pos(query
.start
.line
- -offsetLines
, query
.start
.ch
);
501 query
.end
= Pos(query
.end
.line
- offsetLines
, query
.end
.ch
);
503 files
.push({type
: "full",
505 text
: docValue(ts
, doc
)});
506 query
.file
= doc
.name
;
510 query
.file
= doc
.name
;
512 for (var name
in ts
.docs
) {
513 var cur
= ts
.docs
[name
];
514 if (cur
.changed
&& cur
!= doc
) {
515 files
.push({type
: "full", name
: cur
.name
, text
: docValue(ts
, cur
)});
520 return {query
: query
, files
: files
};
523 function getFragmentAround(data
, start
, end
) {
525 var minIndent
= null, minLine
= null, endLine
, tabSize
= 4;
526 for (var p
= start
.line
- 1, min
= Math
.max(0, p
- 50); p
>= min
; --p
) {
527 var line
= doc
.getLine(p
), fn
= line
.search(/\bfunction\b/);
528 if (fn
< 0) continue;
529 var indent
= CodeMirror
.countColumn(line
, null, tabSize
);
530 if (minIndent
!= null && minIndent
<= indent
) continue;
534 if (minLine
== null) minLine
= min
;
535 var max
= Math
.min(doc
.lastLine(), end
.line
+ 20);
536 if (minIndent
== null || minIndent
== CodeMirror
.countColumn(doc
.getLine(start
.line
), null, tabSize
))
538 else for (endLine
= end
.line
+ 1; endLine
< max
; ++endLine
) {
539 var indent
= CodeMirror
.countColumn(doc
.getLine(endLine
), null, tabSize
);
540 if (indent
<= minIndent
) break;
542 var from = Pos(minLine
, 0);
544 return {type
: "part",
546 offsetLines
: from.line
,
547 text
: doc
.getRange(from, Pos(endLine
, 0))};
552 var cmpPos
= CodeMirror
.cmpPos
;
554 function elt(tagname
, cls
/*, ... elts*/) {
555 var e
= document
.createElement(tagname
);
556 if (cls
) e
.className
= cls
;
557 for (var i
= 2; i
< arguments
.length
; ++i
) {
558 var elt
= arguments
[i
];
559 if (typeof elt
== "string") elt
= document
.createTextNode(elt
);
565 function dialog(cm
, text
, f
) {
567 cm
.openDialog(text
+ ": <input type=text>", f
);
574 function tempTooltip(cm
, content
) {
575 var where
= cm
.cursorCoords();
576 var tip
= makeTooltip(where
.right
+ 1, where
.bottom
, content
);
578 if (!tip
.parentNode
) return;
579 cm
.off("cursorActivity", clear
);
582 setTimeout(clear
, 1700);
583 cm
.on("cursorActivity", clear
);
586 function makeTooltip(x
, y
, content
) {
587 var node
= elt("div", cls
+ "tooltip", content
);
588 node
.style
.left
= x
+ "px";
589 node
.style
.top
= y
+ "px";
590 document
.body
.appendChild(node
);
594 function remove(node
) {
595 var p
= node
&& node
.parentNode
;
596 if (p
) p
.removeChild(node
);
599 function fadeOut(tooltip
) {
600 tooltip
.style
.opacity
= "0";
601 setTimeout(function() { remove(tooltip
); }, 1100);
604 function showError(ts
, cm
, msg
) {
605 if (ts
.options
.showError
)
606 ts
.options
.showError(cm
, msg
);
608 tempTooltip(cm
, String(msg
));
611 function closeArgHints(ts
) {
612 if (ts
.activeArgHints
) { remove(ts
.activeArgHints
); ts
.activeArgHints
= null; }
615 function docValue(ts
, doc
) {
616 var val
= doc
.doc
.getValue();
617 if (ts
.options
.fileFilter
) val
= ts
.options
.fileFilter(val
, doc
.name
, doc
.doc
);
623 function WorkerServer(ts
) {
624 var worker
= new Worker(ts
.options
.workerScript
);
625 worker
.postMessage({type
: "init",
626 defs
: ts
.options
.defs
,
627 plugins
: ts
.options
.plugins
,
628 scripts
: ts
.options
.workerDeps
});
629 var msgId
= 0, pending
= {};
631 function send(data
, c
) {
636 worker
.postMessage(data
);
638 worker
.onmessage = function(e
) {
640 if (data
.type
== "getFile") {
641 getFile(ts
, data
.name
, function(err
, text
) {
642 send({type
: "getFile", err
: String(err
), text
: text
, id
: data
.id
});
644 } else if (data
.type
== "debug") {
645 window
.console
.log(data
.message
);
646 } else if (data
.id
&& pending
[data
.id
]) {
647 pending
[data
.id
](data
.err
, data
.body
);
648 delete pending
[data
.id
];
651 worker
.onerror = function(e
) {
652 for (var id
in pending
) pending
[id
](e
);
656 this.addFile = function(name
, text
) { send({type
: "add", name
: name
, text
: text
}); };
657 this.delFile = function(name
) { send({type
: "del", name
: name
}); };
658 this.request = function(body
, c
) { send({type
: "req", body
: body
}, c
); };