@@ -206,6 +206,259 @@ defmodule Code do
206206 required_files ( )
207207 end
208208
209+ @ doc """
210+ Receives a string and returns the cursor context.
211+
212+ This function receives a string with incomplete Elixir code,
213+ representing a cursor position, and based on the string, it
214+ provides contextual information about said position. The
215+ return of this function can then be used to provide tips,
216+ suggestions, and autocompletion functionality.
217+
218+ This function provides a best-effort detection and may not be
219+ accurate under certain circumstances. See the "Limitations"
220+ section below.
221+
222+ Consider adding a catch-all clause when handling the return
223+ type of this function as new cursor information may be added
224+ in future releases.
225+
226+ ## Examples
227+
228+ iex> Code.cursor_context("")
229+ :expr
230+
231+ iex> Code.cursor_context("hello_wor")
232+ {:local_or_var, 'hello_wor'}
233+
234+ ## Return values
235+
236+ * `{:alias, charlist}` - the context is an alias, potentially
237+ a nested one, such as `Hello.Wor` or `HelloWor`
238+
239+ * `{:dot, inside_dot, charlist}` - the context is a dot
240+ where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
241+ `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot
242+ itself. If a var is given, this may either be a remote call or a map
243+ field access. Examples are `Hello.wor`, `:hello.wor`, `hello.wor`,
244+ `Hello.nested.wor`, `hello.nested.wor`, and `@hello.world`
245+
246+ * `{:dot_arity, inside_dot, charlist}` - the context is a dot arity
247+ where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
248+ `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
249+ itself. If a var is given, it must be a remote arity. Examples are
250+ `Hello.world/`, `:hello.world/`, `hello.world/2`, and `@hello.world/2
251+
252+ * `{:dot_call, inside_dot, charlist}` - the context is a dot
253+ call. This means parentheses or space have been added after the expression.
254+ where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
255+ `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
256+ itself. If a var is given, it must be a remote call. Examples are
257+ `Hello.world(`, `:hello.world(`, `Hello.world `, `hello.world(`, `hello.world `,
258+ and `@hello.world(`
259+
260+ * `:expr` - may be any expression. Autocompletion may suggest an alias,
261+ local or var
262+
263+ * `{:local_or_var, charlist}` - the context is a variable or a local
264+ (import or local) call, such as `hello_wor`
265+
266+ * `{:local_arity, charlist}` - the context is a local (import or local)
267+ call, such as `hello_world/`
268+
269+ * `{:local_call, charlist}` - the context is a local (import or local)
270+ call, such as `hello_world(` and `hello_world `
271+
272+ * `{:module_attribute, charlist}` - the context is a module attribute, such
273+ as `@hello_wor`
274+
275+ * `:none` - no context possible
276+
277+ * `:unquoted_atom` - the context is an unquoted atom. This can be either
278+ previous atoms or all available `:erlang` modules
279+
280+ ## Limitations
281+
282+ * There is no context for operators
283+ * The current algorithm only considers the last line of the input
284+ * Context does not yet track strings, sigils, etc.
285+ * Arguments of functions calls are not currently recognized
286+
287+ """
288+ @ doc since: "1.12.0"
289+ @ spec cursor_context ( List.Chars . t ( ) , keyword ( ) ) ::
290+ { :alias , charlist }
291+ | { :dot , inside_dot , charlist }
292+ | { :dot_arity , inside_dot , charlist }
293+ | { :dot_call , inside_dot , charlist }
294+ | :expr
295+ | { :local_or_var , charlist }
296+ | { :local_arity , charlist }
297+ | { :local_call , charlist }
298+ | { :module_attribute , charlist }
299+ | :none
300+ | { :unquoted_atom , charlist }
301+ when inside_dot:
302+ { :alias , charlist }
303+ | { :dot , inside_dot , charlist }
304+ | { :module_attribute , charlist }
305+ | { :unquoted_atom , charlist }
306+ | { :var , charlist }
307+ def cursor_context ( string , opts \\ [ ] )
308+
309+ def cursor_context ( binary , opts ) when is_binary ( binary ) and is_list ( opts ) do
310+ binary =
311+ case :binary . matches ( binary , "\n " ) do
312+ [ ] ->
313+ binary
314+
315+ matches ->
316+ { position , _ } = List . last ( matches )
317+ binary_part ( binary , position + 1 , byte_size ( binary ) - position - 1 )
318+ end
319+
320+ do_cursor_context ( String . to_charlist ( binary ) , opts )
321+ end
322+
323+ def cursor_context ( charlist , opts ) when is_list ( charlist ) and is_list ( opts ) do
324+ chunked = Enum . chunk_by ( charlist , & ( & 1 == ?\n ) )
325+
326+ case List . last ( chunked , [ ] ) do
327+ [ ?\n | _ ] -> do_cursor_context ( [ ] , opts )
328+ rest -> do_cursor_context ( rest , opts )
329+ end
330+ end
331+
332+ def cursor_context ( other , opts ) do
333+ cursor_context ( to_charlist ( other ) , opts )
334+ end
335+
336+ @ operators '\\ <>+-*/:=|&~^@%'
337+ @ non_closing_punctuation '.,([{;'
338+ @ closing_punctuation ')]}'
339+ @ space '\t \s '
340+ @ closing_identifier '?!'
341+
342+ @ operators_and_non_closing_puctuation @ operators ++ @ non_closing_punctuation
343+ @ non_identifier @ closing_identifier ++
344+ @ operators ++ @ non_closing_punctuation ++ @ closing_punctuation ++ @ space
345+
346+ defp do_cursor_context ( list , _opts ) do
347+ reverse = Enum . reverse ( list )
348+
349+ case strip_spaces ( reverse , 0 ) do
350+ # It is empty
351+ { [ ] , _ } ->
352+ :expr
353+
354+ { [ ?: | _ ] , 0 } ->
355+ { :unquoted_atom , '' }
356+
357+ { [ ?@ | _ ] , 0 } ->
358+ { :module_attribute , '' }
359+
360+ { [ ?. | rest ] , _ } ->
361+ dot ( rest , '' )
362+
363+ # It is a local or remote call with parens
364+ { [ ?( | rest ] , _ } ->
365+ call_to_cursor_context ( rest )
366+
367+ # A local arity definition
368+ { [ ?/ | rest ] , _ } ->
369+ case identifier_to_cursor_context ( rest ) do
370+ { :local_or_var , acc } -> { :local_arity , acc }
371+ { :dot , base , acc } -> { :dot_arity , base , acc }
372+ _ -> :none
373+ end
374+
375+ # Starting a new expression
376+ { [ h | _ ] , _ } when h in @ operators_and_non_closing_puctuation ->
377+ :expr
378+
379+ # It is a local or remote call without parens
380+ { rest , spaces } when spaces > 0 ->
381+ call_to_cursor_context ( rest )
382+
383+ # It is an identifier
384+ _ ->
385+ identifier_to_cursor_context ( reverse )
386+ end
387+ end
388+
389+ defp strip_spaces ( [ h | rest ] , count ) when h in @ space , do: strip_spaces ( rest , count + 1 )
390+ defp strip_spaces ( rest , count ) , do: { rest , count }
391+
392+ defp call_to_cursor_context ( reverse ) do
393+ case identifier_to_cursor_context ( reverse ) do
394+ { :local_or_var , acc } -> { :local_call , acc }
395+ { :dot , base , acc } -> { :dot_call , base , acc }
396+ _ -> :none
397+ end
398+ end
399+
400+ defp identifier_to_cursor_context ( reverse ) do
401+ case identifier ( reverse ) do
402+ # Parse :: first to avoid ambiguity with atoms
403+ { :alias , false , '::' ++ _ , _ } -> :none
404+ { kind , _ , '::' ++ _ , acc } -> alias_or_local_or_var ( kind , acc )
405+ # Now handle atoms, any other atom is unexpected
406+ { _kind , _ , ':' ++ _ , acc } -> { :unquoted_atom , acc }
407+ { :atom , _ , _ , _ } -> :none
408+ # Parse .. first to avoid ambiguity with dots
409+ { :alias , false , _ , _ } -> :none
410+ { kind , _ , '..' ++ _ , acc } -> alias_or_local_or_var ( kind , acc )
411+ # Module attributes
412+ { :alias , _ , '@' ++ _ , _ } -> :none
413+ { :identifier , _ , '@' ++ _ , acc } -> { :module_attribute , acc }
414+ # Everything else
415+ { :alias , _ , '.' ++ rest , acc } -> nested_alias ( rest , acc )
416+ { :identifier , _ , '.' ++ rest , acc } -> dot ( rest , acc )
417+ { kind , _ , _ , acc } -> alias_or_local_or_var ( kind , acc )
418+ :none -> :none
419+ end
420+ end
421+
422+ defp nested_alias ( rest , acc ) do
423+ case identifier_to_cursor_context ( rest ) do
424+ { :alias , prev } -> { :alias , prev ++ '.' ++ acc }
425+ _ -> :none
426+ end
427+ end
428+
429+ defp dot ( rest , acc ) do
430+ case identifier_to_cursor_context ( rest ) do
431+ { :local_or_var , prev } -> { :dot , { :var , prev } , acc }
432+ { :unquoted_atom , _ } = prev -> { :dot , prev , acc }
433+ { :alias , _ } = prev -> { :dot , prev , acc }
434+ { :dot , _ , _ } = prev -> { :dot , prev , acc }
435+ { :module_attribute , _ } = prev -> { :dot , prev , acc }
436+ _ -> :none
437+ end
438+ end
439+
440+ defp alias_or_local_or_var ( :alias , acc ) , do: { :alias , acc }
441+ defp alias_or_local_or_var ( :identifier , acc ) , do: { :local_or_var , acc }
442+ defp alias_or_local_or_var ( _ , _ ) , do: :none
443+
444+ defp identifier ( [ ?? | rest ] ) , do: check_identifier ( rest , [ ?? ] )
445+ defp identifier ( [ ?! | rest ] ) , do: check_identifier ( rest , [ ?! ] )
446+ defp identifier ( rest ) , do: check_identifier ( rest , [ ] )
447+
448+ defp check_identifier ( [ h | _ ] , _acc ) when h in @ non_identifier , do: :none
449+ defp check_identifier ( rest , acc ) , do: rest_identifier ( rest , acc )
450+
451+ defp rest_identifier ( [ h | rest ] , acc ) when h not in @ non_identifier do
452+ rest_identifier ( rest , [ h | acc ] )
453+ end
454+
455+ defp rest_identifier ( rest , acc ) do
456+ case String.Tokenizer . tokenize ( acc ) do
457+ { kind , _ , [ ] , _ , ascii_only? , _ } -> { kind , ascii_only? , rest , acc }
458+ _ -> :none
459+ end
460+ end
461+
209462 @ doc """
210463 Removes files from the required files list.
211464
0 commit comments