@@ -128,6 +128,7 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
128
128
// This can be made much more performant if the tree follows specific invariants.
129
129
$ node = $ doc ->getNodeAtPosition ($ pos );
130
130
131
+ // Get the node at the position under the cursor
131
132
$ offset = $ node === null ? -1 : $ pos ->toOffset ($ node ->getFileContents ());
132
133
if (
133
134
$ node !== null
@@ -148,22 +149,31 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
148
149
$ node = $ node ->parent ;
149
150
}
150
151
152
+ // Inspect the type of expression under the cursor
153
+
151
154
if ($ node === null || $ node instanceof Node \Statement \InlineHtml || $ pos == new Position (0 , 0 )) {
155
+ // HTML, beginning of file
156
+
157
+ // Inside HTML and at the beginning of the file, propose <?php
152
158
$ item = new CompletionItem ('<?php ' , CompletionItemKind::KEYWORD );
153
159
$ item ->textEdit = new TextEdit (
154
160
new Range ($ pos , $ pos ),
155
161
stripStringOverlap ($ doc ->getRange (new Range (new Position (0 , 0 ), $ pos )), '<?php ' )
156
162
);
157
163
$ list ->items [] = $ item ;
158
- } /*
159
-
160
- VARIABLES */
161
- elseif (
162
- $ node instanceof Node \Expression \Variable &&
163
- !(
164
- $ node ->parent instanceof Node \Expression \ScopedPropertyAccessExpression &&
165
- $ node ->parent ->memberName === $ node )
164
+
165
+ } elseif (
166
+ $ node instanceof Node \Expression \Variable
167
+ && !(
168
+ $ node ->parent instanceof Node \Expression \ScopedPropertyAccessExpression
169
+ && $ node ->parent ->memberName === $ node
170
+ )
166
171
) {
172
+ // Variables
173
+ //
174
+ // $|
175
+ // $a|
176
+
167
177
// Find variables, parameters and use statements in the scope
168
178
$ namePrefix = $ node ->getName () ?? '' ;
169
179
foreach ($ this ->suggestVariablesAtNode ($ node , $ namePrefix ) as $ var ) {
@@ -178,138 +188,189 @@ public function provideCompletion(PhpDocument $doc, Position $pos): CompletionLi
178
188
);
179
189
$ list ->items [] = $ item ;
180
190
}
181
- } /*
182
191
183
- MEMBER ACCESS EXPRESSIONS
184
- $a->c#
185
- $a-># */
186
- elseif ($ node instanceof Node \Expression \MemberAccessExpression) {
192
+ } elseif ($ node instanceof Node \Expression \MemberAccessExpression) {
193
+ // Member access expressions
194
+ //
195
+ // $a->c|
196
+ // $a->|
197
+
198
+ // Multiple prefixes for all possible types
187
199
$ prefixes = FqnUtilities \getFqnsFromType (
188
200
$ this ->definitionResolver ->resolveExpressionNodeToType ($ node ->dereferencableExpression )
189
201
);
202
+
203
+ // Include parent classes
190
204
$ prefixes = $ this ->expandParentFqns ($ prefixes );
191
205
206
+ // Add the object access operator to only get members
192
207
foreach ($ prefixes as &$ prefix ) {
193
208
$ prefix .= '-> ' ;
194
209
}
195
-
196
210
unset($ prefix );
197
211
212
+ // Collect all definitions that match any of the prefixes
198
213
foreach ($ this ->index ->getDefinitions () as $ fqn => $ def ) {
199
214
foreach ($ prefixes as $ prefix ) {
200
215
if (substr ($ fqn , 0 , strlen ($ prefix )) === $ prefix && !$ def ->isGlobal ) {
201
216
$ list ->items [] = CompletionItem::fromDefinition ($ def );
202
217
}
203
218
}
204
219
}
205
- } /*
206
-
207
- SCOPED PROPERTY ACCESS EXPRESSIONS
208
- A\B\C::$a#
209
- A\B\C::#
210
- A\B\C::$#
211
- A\B\C::foo#
212
- TODO: $a::# */
213
- elseif (
220
+
221
+ } elseif (
214
222
($ scoped = $ node ->parent ) instanceof Node \Expression \ScopedPropertyAccessExpression ||
215
223
($ scoped = $ node ) instanceof Node \Expression \ScopedPropertyAccessExpression
216
224
) {
225
+ // Static class members and constants
226
+ //
227
+ // A\B\C::$a|
228
+ // A\B\C::|
229
+ // A\B\C::$|
230
+ // A\B\C::foo|
231
+ //
232
+ // TODO: $a::|
233
+
234
+ // Resolve all possible types to FQNs
217
235
$ prefixes = FqnUtilities \getFqnsFromType (
218
236
$ classType = $ this ->definitionResolver ->resolveExpressionNodeToType ($ scoped ->scopeResolutionQualifier )
219
237
);
220
238
239
+ // Add parent classes
221
240
$ prefixes = $ this ->expandParentFqns ($ prefixes );
222
241
242
+ // Append :: operator to only get static members
223
243
foreach ($ prefixes as &$ prefix ) {
224
244
$ prefix .= ':: ' ;
225
245
}
226
-
227
246
unset($ prefix );
228
247
248
+ // Collect all definitions that match any of the prefixes
229
249
foreach ($ this ->index ->getDefinitions () as $ fqn => $ def ) {
230
250
foreach ($ prefixes as $ prefix ) {
231
251
if (substr (strtolower ($ fqn ), 0 , strlen ($ prefix )) === strtolower ($ prefix ) && !$ def ->isGlobal ) {
232
252
$ list ->items [] = CompletionItem::fromDefinition ($ def );
233
253
}
234
254
}
235
255
}
236
- } elseif (ParserHelpers \isConstantFetch ($ node ) ||
237
- ($ creation = $ node ->parent ) instanceof Node \Expression \ObjectCreationExpression ||
238
- (($ creation = $ node ) instanceof Node \Expression \ObjectCreationExpression)) {
239
- $ class = isset ($ creation ) ? $ creation ->classTypeDesignator : $ node ;
240
256
241
- $ prefix = $ class instanceof Node \QualifiedName
242
- ? (string )PhpParser \ResolvedName::buildName ($ class ->nameParts , $ class ->getFileContents ())
243
- : $ class ->getText ($ node ->getFileContents ());
257
+ } elseif (
258
+ ParserHelpers \isConstantFetch ($ node )
259
+ // Creation gets set in case of an instantiation (`new` expression)
260
+ || ($ creation = $ node ->parent ) instanceof Node \Expression \ObjectCreationExpression
261
+ || (($ creation = $ node ) instanceof Node \Expression \ObjectCreationExpression)
262
+ ) {
263
+ // Class instantiations, function calls, constant fetches, class names
264
+ //
265
+ // new MyCl|
266
+ // my_func|
267
+ // MY_CONS|
268
+ // MyCla|
269
+
270
+ // The name Node under the cursor
271
+ $ nameNode = isset ($ creation ) ? $ creation ->classTypeDesignator : $ node ;
272
+
273
+ /** The typed name */
274
+ $ prefix = $ nameNode instanceof Node \QualifiedName
275
+ ? (string )PhpParser \ResolvedName::buildName ($ nameNode ->nameParts , $ nameNode ->getFileContents ())
276
+ : $ nameNode ->getText ($ node ->getFileContents ());
277
+ $ prefixLen = strlen ($ prefix );
278
+
279
+ /** Whether the prefix is qualified (contains at least one backslash) */
280
+ $ isQualified = $ nameNode instanceof Node \QualifiedName && $ nameNode ->isQualifiedName ();
281
+
282
+ /** Whether the prefix is fully qualified (begins with a backslash) */
283
+ $ isFullyQualified = $ nameNode instanceof Node \QualifiedName && $ nameNode ->isFullyQualifiedName ();
284
+
285
+ /** The closest NamespaceDefinition Node */
286
+ $ namespaceNode = $ node ->getNamespaceDefinition ();
287
+
288
+ /** @var string The name of the namespace */
289
+ $ namespacedPrefix = null ;
290
+ if ($ namespaceNode ) {
291
+ $ namespacedPrefix = (string )PhpParser \ResolvedName::buildName ($ namespaceNode ->name ->nameParts , $ node ->getFileContents ()) . '\\' . $ prefix ;
292
+ $ namespacedPrefixLen = strlen ($ namespacedPrefix );
293
+ }
294
+
295
+ // Get the namespace use statements
296
+ // TODO: use function statements, use const statements
244
297
245
- $ namespaceDefinition = $ node ->getNamespaceDefinition ();
298
+ /** @var string[] $aliases A map from local alias to fully qualified name */
299
+ list ($ aliases ,,) = $ node ->getImportTablesForCurrentScope ();
246
300
247
- list ($ namespaceImportTable ,,) = $ node ->getImportTablesForCurrentScope ();
248
- foreach ($ namespaceImportTable as $ alias => $ name ) {
249
- $ namespaceImportTable [$ alias ] = (string )$ name ;
301
+ foreach ($ aliases as $ alias => $ name ) {
302
+ $ aliases [$ alias ] = (string )$ name ;
250
303
}
251
304
252
- foreach ($ this ->index ->getDefinitions () as $ fqn => $ def ) {
253
- $ fqnStartsWithPrefix = substr ($ fqn , 0 , strlen ($ prefix )) === $ prefix ;
254
- $ fqnContainsPrefix = empty ($ prefix ) || strpos ($ fqn , $ prefix ) !== false ;
255
- if (($ def ->canBeInstantiated || ($ def ->isGlobal && !isset ($ creation ))) && $ fqnContainsPrefix ) {
256
- if ($ namespaceDefinition !== null && $ namespaceDefinition ->name !== null ) {
257
- $ namespacePrefix = (string )PhpParser \ResolvedName::buildName ($ namespaceDefinition ->name ->nameParts , $ node ->getFileContents ());
258
-
259
- $ isAliased = false ;
260
-
261
- $ isNotFullyQualified = !($ class instanceof Node \QualifiedName) || !$ class ->isFullyQualifiedName ();
262
- if ($ isNotFullyQualified ) {
263
- foreach ($ namespaceImportTable as $ alias => $ name ) {
264
- if (substr ($ fqn , 0 , strlen ($ name )) === $ name ) {
265
- $ fqn = $ alias ;
266
- $ isAliased = true ;
267
- break ;
268
- }
269
- }
270
- }
271
-
272
- $ prefixWithNamespace = $ namespacePrefix . "\\" . $ prefix ;
273
- $ fqnMatchesPrefixWithNamespace = substr ($ fqn , 0 , strlen ($ prefixWithNamespace )) === $ prefixWithNamespace ;
274
- $ isFullyQualifiedAndPrefixMatches = !$ isNotFullyQualified && ($ fqnStartsWithPrefix || $ fqnMatchesPrefixWithNamespace );
275
- if (!$ isFullyQualifiedAndPrefixMatches && !$ isAliased ) {
276
- if (!array_search ($ fqn , array_values ($ namespaceImportTable ))) {
277
- if (empty ($ prefix )) {
278
- $ fqn = '\\' . $ fqn ;
279
- } elseif ($ fqnMatchesPrefixWithNamespace ) {
280
- $ fqn = substr ($ fqn , strlen ($ namespacePrefix ) + 1 );
281
- } else {
282
- continue ;
283
- }
284
- } else {
285
- continue ;
286
- }
287
- }
288
- } elseif ($ fqnStartsWithPrefix && $ class instanceof Node \QualifiedName && $ class ->isFullyQualifiedName ()) {
289
- $ fqn = '\\' . $ fqn ;
305
+ // If there is a prefix that does not start with a slash, suggest `use`d symbols
306
+ if ($ prefix && !$ isFullyQualified ) {
307
+ foreach ($ aliases as $ alias => $ fqn ) {
308
+ // Suggest symbols that have been `use`d and match the prefix
309
+ if (substr ($ alias , 0 , $ prefixLen ) === $ prefix && ($ def = $ this ->index ->getDefinition ($ fqn ))) {
310
+ $ list ->items [] = CompletionItem::fromDefinition ($ def );
290
311
}
312
+ }
313
+ }
291
314
292
- $ item = CompletionItem::fromDefinition ($ def );
315
+ // Suggest global symbols that either
316
+ // - start with the current namespace + prefix, if the Name node is not fully qualified
317
+ // - start with just the prefix, if the Name node is fully qualified
318
+ foreach ($ this ->index ->getDefinitions () as $ fqn => $ def ) {
293
319
294
- $ item ->insertText = $ fqn ;
320
+ $ fqnStartsWithPrefix = substr ($ fqn , 0 , $ prefixLen ) === $ prefix ;
321
+
322
+ if (
323
+ // Exclude methods, properties etc.
324
+ $ def ->isGlobal
325
+ && (
326
+ !$ prefix
327
+ || (
328
+ // Either not qualified, but a matching prefix with global fallback
329
+ ($ def ->roamed && !$ isQualified && $ fqnStartsWithPrefix )
330
+ // Or not in a namespace or a fully qualified name or AND matching the prefix
331
+ || ((!$ namespaceNode || $ isFullyQualified ) && $ fqnStartsWithPrefix )
332
+ // Or in a namespace, not fully qualified and matching the prefix + current namespace
333
+ || (
334
+ $ namespaceNode
335
+ && !$ isFullyQualified
336
+ && substr ($ fqn , 0 , $ namespacedPrefixLen ) === $ namespacedPrefix
337
+ )
338
+ )
339
+ )
340
+ // Only suggest classes for `new`
341
+ && (!isset ($ creation ) || $ def ->canBeInstantiated )
342
+ ) {
343
+ $ item = CompletionItem::fromDefinition ($ def );
344
+ // Find the shortest name to reference the symbol
345
+ if ($ namespaceNode && ($ alias = array_search ($ fqn , $ aliases , true )) !== false ) {
346
+ // $alias is the name under which this definition is aliased in the current namespace
347
+ $ item ->insertText = $ alias ;
348
+ } else if ($ namespaceNode && !($ prefix && $ isFullyQualified )) {
349
+ // Insert the global FQN with leading backslash
350
+ $ item ->insertText = '\\' . $ fqn ;
351
+ } else {
352
+ // Insert the FQN without leading backlash
353
+ $ item ->insertText = $ fqn ;
354
+ }
355
+ // Don't insert the parenthesis for functions
356
+ // TODO return a snippet and put the cursor inside
357
+ if (substr ($ item ->insertText , -2 ) === '() ' ) {
358
+ $ item ->insertText = substr ($ item ->insertText , 0 , -2 );
359
+ }
295
360
$ list ->items [] = $ item ;
296
361
}
297
362
}
298
363
364
+ // If not a class instantiation, also suggest keywords
299
365
if (!isset ($ creation )) {
300
366
foreach (self ::KEYWORDS as $ keyword ) {
301
- $ item = new CompletionItem ($ keyword , CompletionItemKind::KEYWORD );
302
- $ item ->insertText = $ keyword . ' ' ;
303
- $ list ->items [] = $ item ;
367
+ if (substr ($ keyword , 0 , $ prefixLen ) === $ prefix ) {
368
+ $ item = new CompletionItem ($ keyword , CompletionItemKind::KEYWORD );
369
+ $ item ->insertText = $ keyword ;
370
+ $ list ->items [] = $ item ;
371
+ }
304
372
}
305
373
}
306
- } elseif (ParserHelpers \isConstantFetch ($ node )) {
307
- $ prefix = (string ) ($ node ->getResolvedName () ?? PhpParser \ResolvedName::buildName ($ node ->nameParts , $ node ->getFileContents ()));
308
- foreach (self ::KEYWORDS as $ keyword ) {
309
- $ item = new CompletionItem ($ keyword , CompletionItemKind::KEYWORD );
310
- $ item ->insertText = $ keyword . ' ' ;
311
- $ list ->items [] = $ item ;
312
- }
313
374
}
314
375
315
376
return $ list ;
0 commit comments