From b8c74e7c95c66b2996e3475ef7544f2371aceb20 Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Sat, 8 Mar 2025 12:48:20 -0800 Subject: [PATCH] Handle newlines inside an IEx prompt When given text like: ``` iex> existing_zipper = (""" ...> IO.inspect("Hello, world!") ...> IO.puts("abc") ...> """ ``` We need to ensure that the newlines are preserved in the selectable text --- lib/makeup/lexers/elixir_lexer.ex | 11 +++++++++ .../elixir_lexer_tokenizer_test.exs | 24 ++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/makeup/lexers/elixir_lexer.ex b/lib/makeup/lexers/elixir_lexer.ex index 2f12c0a..0ba80df 100644 --- a/lib/makeup/lexers/elixir_lexer.ex +++ b/lib/makeup/lexers/elixir_lexer.ex @@ -520,6 +520,17 @@ defmodule Makeup.Lexers.ElixirLexer do [first, second | extra_tokens ++ [end_sigil | postprocess_helper(rest)]] end + # When parsing a ...> from a string we need to ensure that the newline is still selectable + defp postprocess_helper([ + {:generic_prompt, %{language: :elixir} = meta, ["\n..." | rest_text]} + | rest + ]) do + newline = {:generic_prompt, %{language: :elixir, selectable: true}, ["\n"]} + # Remove the \n from the ...> prompt + first = {:generic_prompt, meta, ["..." | rest_text]} + [newline, first | postprocess_helper(rest)] + end + defp postprocess_helper([{:string_sigil, attrs, content} | tokens]) do # content is a list of the format ["~", sigil_char, separator, ... sigil_content ..., end_separator] sigil = diff --git a/test/makeup/lexers/elixir_lexer/elixir_lexer_tokenizer_test.exs b/test/makeup/lexers/elixir_lexer/elixir_lexer_tokenizer_test.exs index 6dfe576..963a189 100644 --- a/test/makeup/lexers/elixir_lexer/elixir_lexer_tokenizer_test.exs +++ b/test/makeup/lexers/elixir_lexer/elixir_lexer_tokenizer_test.exs @@ -553,13 +553,17 @@ defmodule ElixirLexerTokenizerTestSnippet do {:operator, %{}, "="}, {:whitespace, %{}, " "}, {:string, %{}, "\""}, - {:generic_prompt, %{selectable: false}, "\n...> "}, + {:generic_prompt, %{selectable: true}, "\n"}, + {:generic_prompt, %{selectable: false}, "...> "}, {:string, %{}, "ine1"}, - {:generic_prompt, %{selectable: false}, "\n...> "}, + {:generic_prompt, %{selectable: true}, "\n"}, + {:generic_prompt, %{selectable: false}, "...> "}, {:string, %{}, "line2"}, - {:generic_prompt, %{selectable: false}, "\n...> "}, + {:generic_prompt, %{selectable: true}, "\n"}, + {:generic_prompt, %{selectable: false}, "...> "}, {:string, %{}, "ilne3"}, - {:generic_prompt, %{selectable: false}, "\n...> "}, + {:generic_prompt, %{selectable: true}, "\n"}, + {:generic_prompt, %{selectable: false}, "...> "}, {:string, %{}, "\""}, {:whitespace, %{}, "\n"} ] @@ -578,7 +582,7 @@ defmodule ElixirLexerTokenizerTestSnippet do ''' first_prompt = "iex#{prompt_number}> " - other_prompt = "\n...#{prompt_number}> " + other_prompt = "...#{prompt_number}> " assert [ {:generic_prompt, %{selectable: false}, ^first_prompt}, @@ -587,12 +591,16 @@ defmodule ElixirLexerTokenizerTestSnippet do {:operator, %{}, "="}, {:whitespace, %{}, " "}, {ttype, %{}, ^ldelim}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line1"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line2"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line3"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, ^rdelim}, {:whitespace, %{}, "\n"} @@ -618,7 +626,7 @@ defmodule ElixirLexerTokenizerTestSnippet do ''' first_prompt = "iex#{prompt_number}> " - other_prompt = "\n...#{prompt_number}> " + other_prompt = "...#{prompt_number}> " sigil_start = sigil_prefix <> ldelim @@ -629,12 +637,16 @@ defmodule ElixirLexerTokenizerTestSnippet do {:operator, %{}, "="}, {:whitespace, %{}, " "}, {ttype, %{}, ^sigil_start}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line1"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line2"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, "line3"}, + {:generic_prompt, %{selectable: true}, "\n"}, {:generic_prompt, %{selectable: false}, ^other_prompt}, {ttype, %{}, ^rdelim}, {:whitespace, %{}, "\n"}