Skip to content

Commit d74c970

Browse files
authored
Snow 2126722 editor command for repl (#2577)
1 parent ae849b2 commit d74c970

12 files changed

+986
-380
lines changed

RELEASE-NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
## Deprecations
2020

2121
## New additions
22+
* The `!edit` command for external editors was added to REPL
2223

2324
## Fixes and improvements
2425
* Improved parsing `!source` with trailing comments
26+
* `!` commands no longer require trailing `;` for evaluation
2527
* Bumped to `typer=0.17.3`. Improved displaying help messages.
2628
* Fixed using `ctx.var` in `snow sql` with Jinja templating.
2729

performance_history_analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import typer
2323
from git import Commit, Repo
24-
from rich import print
24+
from rich import print # noqa: A004
2525

2626

2727
def _reinstall_snowcli() -> None:

src/snowflake/cli/_plugins/sql/repl.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from contextlib import contextmanager
12
from logging import getLogger
23
from typing import Iterable
34

@@ -10,6 +11,7 @@
1011
from snowflake.cli._app.printing import print_result
1112
from snowflake.cli._plugins.sql.lexer import CliLexer, cli_completer
1213
from snowflake.cli._plugins.sql.manager import SqlManager
14+
from snowflake.cli._plugins.sql.repl_commands import detect_command
1315
from snowflake.cli.api.cli_global_context import get_cli_context_manager
1416
from snowflake.cli.api.console import cli_console
1517
from snowflake.cli.api.output.types import MultipleResults, QueryResult
@@ -28,6 +30,21 @@
2830
log.debug("setting history file to: %s", HISTORY_FILE.as_posix())
2931

3032

33+
@contextmanager
34+
def repl_context(repl_instance):
35+
"""Context manager for REPL execution that handles CLI context registration."""
36+
context_manager = get_cli_context_manager()
37+
context_manager.is_repl = True
38+
context_manager.repl_instance = repl_instance
39+
40+
try:
41+
yield
42+
finally:
43+
# Clean up REPL context
44+
context_manager.is_repl = False
45+
context_manager.repl_instance = None
46+
47+
3148
class Repl:
3249
"""Basic REPL implementation for the Snowflake CLI."""
3350

@@ -45,7 +62,6 @@ def __init__(
4562
`retain_comments` how to handle comments in queries
4663
"""
4764
super().__init__()
48-
setattr(get_cli_context_manager(), "is_repl", True)
4965
self._data = data or {}
5066
self._retain_comments = retain_comments
5167
self._template_syntax_config = template_syntax_config
@@ -56,6 +72,7 @@ def __init__(
5672
self._yes_no_keybindings = self._setup_yn_key_bindings()
5773
self._sql_manager = sql_manager
5874
self.session = PromptSession(history=self._history)
75+
self._next_input: str | None = None
5976

6077
def _setup_key_bindings(self) -> KeyBindings:
6178
"""Key bindings for repl. Helps detecting ; at end of buffer."""
@@ -67,19 +84,31 @@ def not_searching():
6784

6885
@kb.add(Keys.Enter, filter=not_searching)
6986
def _(event):
70-
"""Handle Enter key press."""
87+
"""Handle Enter key press with intelligent execution logic.
88+
89+
Execution priority:
90+
1. Exit keywords (exit, quit) - execute immediately
91+
2. REPL commands (starting with !) - execute immediately
92+
3. SQL with trailing semicolon - execute immediately
93+
4. All other input - add new line for multi-line editing
94+
"""
7195
buffer = event.app.current_buffer
7296
stripped_buffer = buffer.text.strip()
7397

7498
if stripped_buffer:
7599
log.debug("evaluating repl input")
76100
cursor_position = buffer.cursor_position
77101
ends_with_semicolon = buffer.text.endswith(";")
102+
is_command = detect_command(stripped_buffer) is not None
78103

79104
if stripped_buffer.lower() in EXIT_KEYWORDS:
80105
log.debug("exit keyword detected %r", stripped_buffer)
81106
buffer.validate_and_handle()
82107

108+
elif is_command:
109+
log.debug("command detected, submitting input")
110+
buffer.validate_and_handle()
111+
83112
elif ends_with_semicolon and cursor_position >= len(stripped_buffer):
84113
log.debug("semicolon detected, submitting input")
85114
buffer.validate_and_handle()
@@ -118,16 +147,27 @@ def _(event):
118147

119148
return kb
120149

121-
def repl_propmpt(self, msg: str = " > ") -> str:
122-
"""Regular repl prompt."""
123-
return self.session.prompt(
124-
msg,
125-
lexer=self._lexer,
126-
completer=self._completer,
127-
multiline=True,
128-
wrap_lines=True,
129-
key_bindings=self._repl_key_bindings,
130-
)
150+
def repl_prompt(self, msg: str = " > ") -> str:
151+
"""Regular repl prompt with support for pre-filled input.
152+
153+
Checks for queued input from commands like !edit and uses it as
154+
default text in the prompt. The queued input is cleared after use.
155+
"""
156+
default_text = self._next_input
157+
158+
try:
159+
return self.session.prompt(
160+
msg,
161+
lexer=self._lexer,
162+
completer=self._completer,
163+
multiline=True,
164+
wrap_lines=True,
165+
key_bindings=self._repl_key_bindings,
166+
default=default_text or "",
167+
)
168+
finally:
169+
if self._next_input == default_text:
170+
self._next_input = None
131171

132172
def yn_prompt(self, msg: str) -> str:
133173
"""Yes/No prompt."""
@@ -142,7 +182,7 @@ def yn_prompt(self, msg: str) -> str:
142182

143183
@property
144184
def _welcome_banner(self) -> str:
145-
return f"Welcome to Snowflake-CLI REPL\nType 'exit' or 'quit' to leave"
185+
return "Welcome to Snowflake-CLI REPL\nType 'exit' or 'quit' to leave"
146186

147187
def _initialize_connection(self):
148188
"""Early connection for possible fast fail."""
@@ -163,12 +203,13 @@ def _execute(self, user_input: str) -> Iterable[SnowflakeCursor]:
163203
return cursors
164204

165205
def run(self):
166-
try:
167-
cli_console.panel(self._welcome_banner)
168-
self._initialize_connection()
169-
self._repl_loop()
170-
except (KeyboardInterrupt, EOFError):
171-
cli_console.message("\n[bold orange_red1]Leaving REPL, bye ...")
206+
with repl_context(self):
207+
try:
208+
cli_console.panel(self._welcome_banner)
209+
self._initialize_connection()
210+
self._repl_loop()
211+
except (KeyboardInterrupt, EOFError):
212+
cli_console.message("\n[bold orange_red1]Leaving REPL, bye ...")
172213

173214
def _repl_loop(self):
174215
"""Main REPL loop. Handles input and query execution.
@@ -178,7 +219,7 @@ def _repl_loop(self):
178219
"""
179220
while True:
180221
try:
181-
user_input = self.repl_propmpt().strip()
222+
user_input = self.repl_prompt().strip()
182223

183224
if not user_input:
184225
continue
@@ -210,6 +251,21 @@ def _repl_loop(self):
210251
except Exception as e:
211252
cli_console.warning(f"\nError occurred: {e}")
212253

254+
def set_next_input(self, text: str) -> None:
255+
"""Set the text that will be used as the next REPL input."""
256+
self._next_input = text
257+
log.debug("Next input has been set")
258+
259+
@property
260+
def next_input(self) -> str | None:
261+
"""Get the next input text that will be used in the prompt."""
262+
return self._next_input
263+
264+
@property
265+
def history(self) -> FileHistory:
266+
"""Get the FileHistory instance used by the REPL."""
267+
return self._history
268+
213269
def ask_yn(self, question: str) -> bool:
214270
"""Asks user a Yes/No question."""
215271
try:

0 commit comments

Comments
 (0)