Skip to content

Commit 28fca1c

Browse files
committed
refactor(commit): simplify multiline input handling logic
Consolidate multiline input logic into dedicated _handle_multiline_question function, removing duplicated code from the main prompt loop. Improve type hints by using Style instead of Any, and refactor error message formatting for consistency.
1 parent 4b65199 commit 28fca1c

File tree

1 file changed

+75
-113
lines changed

1 file changed

+75
-113
lines changed

commitizen/commands/commit.py

Lines changed: 75 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
from typing import Any, TypedDict
1010

1111
import questionary
12-
import questionary.prompts.text
1312
from prompt_toolkit.key_binding import KeyBindings
1413
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
1514
from prompt_toolkit.keys import Keys
15+
from prompt_toolkit.styles import Style
1616

1717
from commitizen import factory, git, out
1818
from commitizen.config import BaseConfig
@@ -45,8 +45,11 @@ class CommitArgs(TypedDict, total=False):
4545
retry: bool
4646

4747

48-
def _handle_questionary_prompt(question: CzQuestion, cz_style: Any) -> dict[str, Any]:
49-
"""Handle questionary prompt with error handling."""
48+
def _handle_questionary_prompt(question: CzQuestion, cz_style: Style) -> dict[str, Any]:
49+
"""Handle questionary prompt with multiline and error handling."""
50+
if question["type"] == "input" and question.get("multiline", False):
51+
return _handle_multiline_question(question, cz_style)
52+
5053
try:
5154
answer = questionary.prompt([question], style=cz_style)
5255
if not answer:
@@ -55,13 +58,76 @@ def _handle_questionary_prompt(question: CzQuestion, cz_style: Any) -> dict[str,
5558
except ValueError as err:
5659
root_err = err.__context__
5760
if isinstance(root_err, CzException):
58-
raise CustomError(root_err.__str__())
61+
raise CustomError(str(root_err))
5962
raise err
6063

6164

62-
def _handle_multiline_fallback(multiline_question: InputQuestion, cz_style: Any) -> dict[str, Any]:
63-
"""Handle fallback to standard behavior if custom multiline approach fails."""
64-
return _handle_questionary_prompt(multiline_question, cz_style)
65+
def _handle_multiline_question(
66+
question: InputQuestion, cz_style: Style
67+
) -> dict[str, Any]:
68+
"""Handle multiline input questions."""
69+
is_optional = (
70+
question.get("default") == ""
71+
or "skip" in question.get("message", "").lower()
72+
or "[enter] to skip" in question.get("message", "").lower()
73+
)
74+
75+
guidance = (
76+
"💡 Press Enter on empty line to skip, Alt+Enter to finish"
77+
if is_optional
78+
else "💡 Press Alt+Enter to finish"
79+
)
80+
out.info(guidance)
81+
82+
def _handle_key_press(event: KeyPressEvent, is_finish_key: bool) -> None:
83+
buffer = event.current_buffer
84+
is_empty = not buffer.text.strip()
85+
86+
if is_empty:
87+
if is_optional and not is_finish_key:
88+
event.app.exit(result="")
89+
elif not is_optional:
90+
out.error(
91+
"⚠ This field is required. Please enter some content or press Ctrl+C to abort."
92+
)
93+
out.line("> ", end="", flush=True)
94+
else:
95+
event.app.exit(result=buffer.text)
96+
else:
97+
if is_finish_key:
98+
event.app.exit(result=buffer.text)
99+
else:
100+
buffer.newline()
101+
102+
bindings = KeyBindings()
103+
104+
@bindings.add(Keys.Enter)
105+
def _(event: KeyPressEvent) -> None:
106+
_handle_key_press(event, is_finish_key=False)
107+
108+
@bindings.add(Keys.Escape, Keys.Enter)
109+
def _(event: KeyPressEvent) -> None:
110+
_handle_key_press(event, is_finish_key=True)
111+
112+
result = questionary.text(
113+
message=question["message"],
114+
multiline=True,
115+
style=cz_style,
116+
key_bindings=bindings,
117+
).unsafe_ask()
118+
119+
if result is None:
120+
result = question.get("default", "")
121+
122+
if "filter" in question:
123+
try:
124+
result = question["filter"](result)
125+
except Exception as e:
126+
out.error(f"⚠ {str(e)}")
127+
out.line("> ", end="", flush=True)
128+
return _handle_multiline_question(question, cz_style)
129+
130+
return {question["name"]: result}
65131

66132

67133
class Commit:
@@ -92,116 +158,12 @@ def _prompt_commit_questions(self) -> str:
92158
questions = cz.questions()
93159
answers = {}
94160

95-
# Handle questions one by one to support custom continuation
96161
for question in questions:
97162
if question["type"] == "list":
98163
question["use_shortcuts"] = self.config.settings["use_shortcuts"]
99-
answer = _handle_questionary_prompt(question, cz.style)
100-
answers.update(answer)
101-
elif question["type"] == "input" and question.get("multiline", False):
102-
is_optional = (
103-
question.get("default") == ""
104-
or "skip" in question.get("message", "").lower()
105-
)
106164

107-
if is_optional:
108-
out.info(
109-
"💡 Multiline input:\n Press Enter on empty line to skip, Enter after text for new lines, Alt+Enter to finish"
110-
)
111-
else:
112-
out.info(
113-
"💡 Multiline input:\n Press Enter for new lines and Alt+Enter to finish"
114-
)
115-
116-
# Create custom multiline input with Enter-on-empty behavior for optional fields
117-
118-
multiline_question = question.copy()
119-
multiline_question["multiline"] = True
120-
121-
if is_optional:
122-
# Create custom key bindings for optional fields
123-
bindings = KeyBindings()
124-
125-
@bindings.add(Keys.Enter)
126-
def _(event: KeyPressEvent) -> None:
127-
buffer = event.current_buffer
128-
# If buffer is completely empty, submit
129-
if not buffer.text.strip():
130-
event.app.exit(result=buffer.text)
131-
else:
132-
# If there's text, add new line
133-
buffer.newline()
134-
135-
# Use the text prompt directly with custom bindings
136-
try:
137-
result = questionary.prompts.text.text(
138-
message=question["message"],
139-
multiline=True,
140-
style=cz.style,
141-
key_bindings=bindings,
142-
).ask()
143-
144-
field_name = question["name"]
145-
if result is None:
146-
result = question.get("default", "")
147-
148-
# Apply filter if present
149-
if "filter" in question:
150-
result = question["filter"](result)
151-
152-
answer = {field_name: result}
153-
answers.update(answer)
154-
155-
except Exception:
156-
# Fallback to standard behavior if custom approach fails
157-
answer = _handle_multiline_fallback(multiline_question, cz.style)
158-
answers.update(answer)
159-
else:
160-
# Required fields - don't allow newline on empty first line and show error
161-
bindings = KeyBindings()
162-
163-
@bindings.add(Keys.Enter)
164-
def _(event: KeyPressEvent) -> None:
165-
buffer = event.current_buffer
166-
# If buffer is completely empty (no content at all), show error and don't allow newline
167-
if not buffer.text.strip():
168-
# Show error message with prompt
169-
out.error(
170-
"\n⚠ This field is required. Please enter some content or press Ctrl+C to abort."
171-
)
172-
print("> ", end="", flush=True)
173-
# Don't do anything - require content first
174-
pass
175-
else:
176-
# If there's text, add new line
177-
buffer.newline()
178-
179-
try:
180-
result = questionary.prompts.text.text(
181-
message=question["message"],
182-
multiline=True,
183-
style=cz.style,
184-
key_bindings=bindings,
185-
).ask()
186-
187-
field_name = question["name"]
188-
if result is None:
189-
result = ""
190-
191-
# Apply filter if present
192-
if "filter" in question:
193-
result = question["filter"](result)
194-
195-
answer = {field_name: result}
196-
answers.update(answer)
197-
198-
except Exception:
199-
# Fallback to standard behavior if custom approach fails
200-
answer = _handle_multiline_fallback(multiline_question, cz.style)
201-
answers.update(answer)
202-
else:
203-
answer = _handle_questionary_prompt(question, cz.style)
204-
answers.update(answer)
165+
answer = _handle_questionary_prompt(question, cz.style)
166+
answers.update(answer)
205167

206168
message = cz.message(answers)
207169
message_len = len(message.partition("\n")[0].strip())

0 commit comments

Comments
 (0)