@@ -99,6 +99,168 @@ define(function (require, exports, module) {
9999 } , "TypeScript type error to be reported" , 30000 ) ;
100100 } , 45000 ) ;
101101
102+ it ( "should reject the parent call's parameter hints inside a callback body only" , async function ( ) {
103+ // tsserver treats the whole callback argument INCLUDING its body as "inside the call",
104+ // so without the body gate the parent signature popup shows (and never dismisses)
105+ // while coding inside the callback. The gate REJECTS the request (instead of declining
106+ // in hasParameterHints) so the manager dismisses the popup without falling through to
107+ // the legacy Tern JS provider. Object-literal arguments must keep their hints.
108+ const tsMain = await new Promise ( function ( resolve , reject ) {
109+ const ExtensionLoader = testWindow . brackets . test . ExtensionLoader ;
110+ const ctx = ExtensionLoader . getRequireContextForExtension ( "TypeScriptSupport" ) ;
111+ ctx ( [ "main" ] , resolve , reject ) ;
112+ } ) ;
113+ const client = tsMain . _getClient ( ) ;
114+ expect ( client ) . toBeTruthy ( ) ;
115+ const provider = client . parameterHints ;
116+ await _openInProject ( "ts/" , "type-error.ts" ) ;
117+ const editor = EditorManager . getActiveEditor ( ) ;
118+ editor . document . setText (
119+ "function withOptions(cb: (n: number) => void, options: { color: string }) { cb(1); }\n" +
120+ "withOptions((n) => {\n" +
121+ " console.log()\n" +
122+ "}, { color: \"red\" });\n"
123+ ) ;
124+
125+ // inside the callback BODY -> the request is rejected outright (popup suppressed /
126+ // auto-dismissed there), while the provider still owns the request
127+ editor . setCursorPos ( 2 , 4 ) ;
128+ expect ( provider . hasParameterHints ( editor , null ) ) . toBe ( true ) ;
129+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . toBe ( "rejected" ) ;
130+
131+ // inside a NESTED call's parens within the body (`console.log(|)`) -> that call's own
132+ // hints must flow; the just-typed "(" is the token at the cursor and must be counted
133+ const logLine = editor . document . getLine ( 2 ) ;
134+ editor . setCursorPos ( 2 , logLine . indexOf ( "(" ) + 1 ) ;
135+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . not . toBe ( "rejected" ) ;
136+
137+ // directly inside the argument list -> hints flow to the server as usual
138+ editor . setCursorPos ( 1 , 12 ) ;
139+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . not . toBe ( "rejected" ) ;
140+
141+ // inside an object-literal argument -> hints still flow
142+ const literalLine = editor . document . getLine ( 3 ) ;
143+ editor . setCursorPos ( 3 , literalLine . indexOf ( "\"red\"" ) ) ;
144+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . not . toBe ( "rejected" ) ;
145+
146+ // minified-style lines (>2000 chars) are never token-scanned (that walk is quadratic
147+ // and can freeze the UI): the popup is suppressed there, EXCEPT directly at a call
148+ // head, which a cheap plain-text window decides
149+ const filler = "x1=1;" . repeat ( 500 ) ; // 2500 chars
150+ const minified = "process.on(\"x\", () => {" + filler + "console.log(" ;
151+ editor . document . setText ( minified ) ;
152+ editor . setCursorPos ( 0 , minified . length ) ; // right after `console.log(`
153+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . not . toBe ( "rejected" ) ;
154+ editor . setCursorPos ( 0 , minified . indexOf ( filler ) + 1000 ) ; // deep in the filler
155+ expect ( provider . getParameterHints ( true , false ) . state ( ) ) . toBe ( "rejected" ) ;
156+
157+ await awaitsForDone ( CommandManager . execute ( Commands . FILE_CLOSE ,
158+ { fullPath : testFolder + "ts/type-error.ts" , _forceClose : true } ) ) ;
159+ } , 45000 ) ;
160+
161+ // Parse a source snippet with a "|" cursor marker into {text, pos}.
162+ function _parseCursorMarker ( src ) {
163+ const idx = src . indexOf ( "|" ) ;
164+ const before = src . substring ( 0 , idx ) ;
165+ const line = before . split ( "\n" ) . length - 1 ;
166+ const ch = line === 0 ? idx : idx - before . lastIndexOf ( "\n" ) - 1 ;
167+ return { text : src . replace ( "|" , "" ) , pos : { line : line , ch : ch } } ;
168+ }
169+
170+ it ( "should classify cursor contexts for the parameter-hint body gate" , async function ( ) {
171+ // Table-driven coverage of _inFunctionBodyInsideArgs. inBody: true means the parent
172+ // call's signature popup is suppressed at the "|" cursor; false means hints flow.
173+ const CASES = [
174+ // function bodies nested in call arguments -> suppress
175+ { name : "blank arrow body" , src : "on(\"x\", () => { | })" , inBody : true } ,
176+ { name : "arrow body after a statement" , src : "on(\"x\", () => { log(\"hi\"); | })" , inBody : true } ,
177+ { name : "multi-line arrow body" , src : "on(\"x\", (a, b) => {\n const c = a + b;\n |\n})" ,
178+ inBody : true } ,
179+ { name : "function-expression body" , src : "arr.map(function (x) { | })" , inBody : true } ,
180+ { name : "if block inside a callback body" , src : "f(x => { if (y) { | } })" , inBody : true } ,
181+ { name : "object method shorthand body" , src : "f({ m() { | } })" , inBody : true } ,
182+ { name : "class method body in class-expression arg" , src : "f(class { m() { | } })" , inBody : true } ,
183+ { name : "statement position after a closed nested call" , src : "f(() => { g(); | })" , inBody : true } ,
184+ // also true with no call around it - harmless, servers offer no signature there
185+ { name : "top-level function body" , src : "function a() { | }" , inBody : true } ,
186+
187+ // argument positions -> hints must flow
188+ { name : "empty argument list" , src : "withOptions(|)" , inBody : false } ,
189+ { name : "second argument" , src : "f(a, |)" , inBody : false } ,
190+ { name : "after a closed nested call, still in args" , src : "f(g(1), |)" , inBody : false } ,
191+ { name : "nested call inside a callback body" , src : "f(() => { console.log(|) })" , inBody : false } ,
192+ { name : "nested call with args inside a body" , src : "f(() => { log(\"a\", |) })" , inBody : false } ,
193+
194+ // object/array literals in arguments -> hints must flow
195+ { name : "object literal argument" , src : "css({ color: | })" , inBody : false } ,
196+ { name : "nested object literal" , src : "cfg({ a: { b: | } })" , inBody : false } ,
197+ { name : "object literal inside an array argument" , src : "f([{ a: | }])" , inBody : false } ,
198+ { name : "parenthesized object literal" , src : "f(({ a: | }))" , inBody : false } ,
199+
200+ // strings and comments are invisible to the scan
201+ { name : "brace inside a string argument" , src : "f(\"some { text\", |)" , inBody : false } ,
202+ { name : "brace inside a template string" , src : "f(`some { brace | text`)" , inBody : false } ,
203+ { name : "brace inside a block comment" , src : "f(/* { */ |2)" , inBody : false } ,
204+ { name : "brace inside a line comment" , src : "f(1, // { comment\n |2)" , inBody : false } ,
205+
206+ // not in any call at all
207+ { name : "top-level statement" , src : "const x = |;" , inBody : false }
208+ ] ;
209+ const tsMain = await new Promise ( function ( resolve , reject ) {
210+ const ExtensionLoader = testWindow . brackets . test . ExtensionLoader ;
211+ const ctx = ExtensionLoader . getRequireContextForExtension ( "TypeScriptSupport" ) ;
212+ ctx ( [ "main" ] , resolve , reject ) ;
213+ } ) ;
214+ await _openInProject ( "ts/" , "type-error.ts" ) ;
215+ const editor = EditorManager . getActiveEditor ( ) ;
216+ const failures = [ ] ;
217+ CASES . forEach ( function ( testCase ) {
218+ const parsed = _parseCursorMarker ( testCase . src ) ;
219+ editor . document . setText ( parsed . text ) ;
220+ editor . setCursorPos ( parsed . pos . line , parsed . pos . ch ) ;
221+ const actual = tsMain . _inFunctionBodyInsideArgs ( editor ) ;
222+ if ( actual !== testCase . inBody ) {
223+ failures . push ( testCase . name + ": expected inBody=" + testCase . inBody + " but got " + actual ) ;
224+ }
225+ } ) ;
226+ expect ( failures ) . toEqual ( [ ] ) ;
227+ await awaitsForDone ( CommandManager . execute ( Commands . FILE_CLOSE ,
228+ { fullPath : testFolder + "ts/type-error.ts" , _forceClose : true } ) ) ;
229+ } , 45000 ) ;
230+
231+ it ( "should decide call heads on minified-style lines via the plain-text window" , async function ( ) {
232+ // Table-driven coverage of _atCallHeadPlainText (used only on >2000-char lines, where
233+ // token-scanning is too expensive). atHead: true means hints are allowed there.
234+ const filler = "x=1;" . repeat ( 80 ) ; // 320 chars of paren/brace-free filler
235+ const CASES = [
236+ { name : "right after an open paren" , line : "foo(" , ch : 4 , atHead : true } ,
237+ { name : "between autoclosed parens" , line : "foo()" , ch : 4 , atHead : true } ,
238+ { name : "after an argument" , line : "foo(a, " , ch : 7 , atHead : true } ,
239+ { name : "after a closed nested call" , line : "foo(bar(1), " , ch : 12 , atHead : true } ,
240+ { name : "after a matched object-literal argument" , line : "foo({a:1}, " , ch : 11 , atHead : true } ,
241+ // conservative on minified lines: any enclosing brace suppresses
242+ { name : "inside a body brace" , line : "f(() => {" + filler , ch : 9 + 40 , atHead : false } ,
243+ { name : "inside an object-literal argument" , line : "foo({ " , ch : 6 , atHead : false } ,
244+ { name : "no parens within the window" , line : filler + filler , ch : 300 , atHead : false } ,
245+ { name : "call paren beyond the 200-char window" , line : "foo(" + "a" . repeat ( 250 ) , ch : 254 ,
246+ atHead : false } ,
247+ { name : "line start" , line : "foo" , ch : 0 , atHead : false }
248+ ] ;
249+ const tsMain = await new Promise ( function ( resolve , reject ) {
250+ const ExtensionLoader = testWindow . brackets . test . ExtensionLoader ;
251+ const ctx = ExtensionLoader . getRequireContextForExtension ( "TypeScriptSupport" ) ;
252+ ctx ( [ "main" ] , resolve , reject ) ;
253+ } ) ;
254+ const failures = [ ] ;
255+ CASES . forEach ( function ( testCase ) {
256+ const actual = tsMain . _atCallHeadPlainText ( testCase . line , testCase . ch ) ;
257+ if ( actual !== testCase . atHead ) {
258+ failures . push ( testCase . name + ": expected atHead=" + testCase . atHead + " but got " + actual ) ;
259+ }
260+ } ) ;
261+ expect ( failures ) . toEqual ( [ ] ) ;
262+ } , 45000 ) ;
263+
102264 it ( "should report implicit-any in a JS project that opts into checkJs" , async function ( ) {
103265 // js-checkjs has a jsconfig.json with checkJs + noImplicitAny, so the untyped parameter
104266 // in implicit.js IS flagged - and our diagnostic filter keeps it (the project opted in).
0 commit comments