Skip to content
34 changes: 34 additions & 0 deletions cli/tools/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use crate::cache::IncrementalCache;
use crate::colors;
use crate::factory::CliFactory;
use crate::sys::CliSys;
use crate::util;
use crate::util::file_watcher;
use crate::util::fs::canonicalize_path;
use crate::util::path::get_extension;
Expand Down Expand Up @@ -165,6 +166,8 @@ fn resolve_paths_with_options_batches(
cli_options: &CliOptions,
fmt_flags: &FmtFlags,
) -> Result<Vec<PathsWithOptions>, AnyError> {
maybe_show_format_confirmation(cli_options, fmt_flags)?;

let members_fmt_options =
cli_options.resolve_fmt_options_for_members(fmt_flags)?;
let mut paths_with_options_batches =
Expand All @@ -186,6 +189,37 @@ fn resolve_paths_with_options_batches(
Ok(paths_with_options_batches)
}

fn maybe_show_format_confirmation(
cli_options: &CliOptions,
fmt_flags: &FmtFlags,
) -> Result<(), AnyError> {
if fmt_flags.check
|| !fmt_flags.files.include.is_empty()
|| cli_options.workspace().deno_jsons().next().is_some()
|| cli_options.workspace().package_jsons().next().is_some()
{
return Ok(());
}

let confirm_result =
util::console::confirm(util::console::ConfirmOptions {
default: true,
message: format!(
"{} It looks like you're not in a workspace. Are you sure you want to format the entire '{}' directory?",
colors::yellow("Warning"),
cli_options.initial_cwd().display()
),
})
.unwrap_or(false);
if confirm_result {
Ok(())
} else {
bail!(
"Did not format non-workspace directory. Run again specifying the current directory (ex. `deno fmt .`)"
)
}
}

async fn format_files(
caches: &Arc<Caches>,
cli_options: &Arc<CliOptions>,
Expand Down
32 changes: 4 additions & 28 deletions cli/tools/pm/outdated/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use unicode_width::UnicodeWidthStr;

use crate::tools::pm::deps::DepId;
use crate::tools::pm::deps::DepKind;
use crate::util::console::HideCursorGuard;
use crate::util::console::RawMode;

#[derive(Debug)]
pub struct PackageInfo {
Expand Down Expand Up @@ -249,31 +251,6 @@ fn highlight_new_version(current: &Version, new: &Version) -> String {
}
}

struct RawMode {
needs_disable: bool,
}

impl RawMode {
fn enable() -> io::Result<Self> {
terminal::enable_raw_mode()?;
Ok(Self {
needs_disable: true,
})
}
fn disable(mut self) -> io::Result<()> {
self.needs_disable = false;
terminal::disable_raw_mode()
}
}

impl Drop for RawMode {
fn drop(&mut self) {
if self.needs_disable {
let _ = terminal::disable_raw_mode();
}
}
}

pub fn select_interactive(
packages: Vec<PackageInfo>,
) -> anyhow::Result<Option<HashSet<DepId>>> {
Expand Down Expand Up @@ -305,7 +282,7 @@ pub fn select_interactive(
}

let mut state = State::new(packages)?;
stderr.execute(cursor::Hide)?;
let hide_cursor_guard = HideCursorGuard::hide()?;

let instructions_width = format!("? {}", State::instructions_line()).width();

Expand Down Expand Up @@ -407,8 +384,7 @@ pub fn select_interactive(

static_text.eprint_clear();

crossterm::execute!(&mut stderr, cursor::Show)?;

hide_cursor_guard.show()?;
raw_mode.disable()?;

if do_it {
Expand Down
151 changes: 151 additions & 0 deletions cli/util/console.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,160 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::io;
use std::sync::Arc;

use crossterm::ExecutableCommand;
use crossterm::cursor;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use crossterm::terminal;
use deno_core::parking_lot::Mutex;
use deno_runtime::ops::tty::ConsoleSize;

use super::draw_thread::DrawThread;

/// Gets the console size.
pub fn console_size() -> Option<ConsoleSize> {
let stderr = &deno_runtime::deno_io::STDERR_HANDLE;
deno_runtime::ops::tty::console_size(stderr).ok()
}

pub struct RawMode {
needs_disable: bool,
}

impl RawMode {
pub fn enable() -> io::Result<Self> {
terminal::enable_raw_mode()?;
Ok(Self {
needs_disable: true,
})
}

pub fn disable(mut self) -> io::Result<()> {
self.needs_disable = false;
terminal::disable_raw_mode()
}
}

impl Drop for RawMode {
fn drop(&mut self) {
if self.needs_disable {
let _ = terminal::disable_raw_mode();
}
}
}

pub struct HideCursorGuard {
needs_disable: bool,
}

impl HideCursorGuard {
pub fn hide() -> io::Result<Self> {
io::stderr().execute(cursor::Hide)?;
Ok(Self {
needs_disable: true,
})
}

pub fn show(mut self) -> io::Result<()> {
self.needs_disable = false;
io::stderr().execute(cursor::Show)?;
Ok(())
}
}

impl Drop for HideCursorGuard {
fn drop(&mut self) {
if self.needs_disable {
_ = io::stderr().execute(cursor::Show);
}
}
}

#[derive(Debug)]
pub struct ConfirmOptions {
pub message: String,
pub default: bool,
}

/// Prompts and confirms if a tty.
///
/// Returns `None` when a tty.
pub fn confirm(options: ConfirmOptions) -> Option<bool> {
#[derive(Debug)]
struct PromptRenderer {
options: ConfirmOptions,
selection: Arc<Mutex<String>>,
}

impl super::draw_thread::DrawThreadRenderer for PromptRenderer {
fn render(&self, _data: &ConsoleSize) -> String {
let is_yes_default = self.options.default;
let selection = self.selection.lock();
format!(
"{} [{}/{}] {}",
self.options.message,
if is_yes_default { "Y" } else { "y" },
if is_yes_default { "n" } else { "N" },
*selection,
)
}
}

if !DrawThread::is_supported() {
return None;
}

let _raw_mode = RawMode::enable().ok()?;
let _hide_cursor_guard = HideCursorGuard::hide().ok()?;
let selection = Arc::new(Mutex::new(String::new()));
let default = options.default;
// uses a renderer and the draw thread in order to allow
// displaying other stuff on the draw thread while the prompt
// is showing
let renderer = PromptRenderer {
options,
selection: selection.clone(),
};
let _state = DrawThread::add_entry(Arc::new(renderer));

let mut selected = default;
loop {
let event = crossterm::event::read().ok()?;
#[allow(clippy::single_match)]
match event {
crossterm::event::Event::Key(KeyEvent {
kind: KeyEventKind::Press,
code,
modifiers,
..
}) => match (code, modifiers) {
(KeyCode::Char('c'), KeyModifiers::CONTROL)
| (KeyCode::Char('q'), KeyModifiers::NONE) => break,
(KeyCode::Char('y'), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
selected = true;
*selection.lock() = "Y".to_string();
}
(KeyCode::Char('n'), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
selected = false;
*selection.lock() = "N".to_string();
}
(KeyCode::Backspace, _) => {
selected = default;
*selection.lock() = "".to_string();
}
// l is common for enter in vim keybindings
(KeyCode::Enter, _) | (KeyCode::Char('l'), KeyModifiers::NONE) => {
return Some(selected);
}
_ => {}
},
_ => {}
}
}

None
}
20 changes: 19 additions & 1 deletion tests/integration/fmt_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ fn fmt_auto_ignore_git_and_node_modules() {
.new_command()
.current_dir(t)
.env("NO_COLOR", "1")
.args("fmt")
.args("fmt .")
.run();

output.assert_exit_code(1);
Expand Down Expand Up @@ -421,3 +421,21 @@ fn opt_out_top_level_exclude_via_fmt_unexclude() {
assert_contains!(output, "excluded.ts");
assert_not_contains!(output, "actually_excluded.ts");
}

#[test]
fn test_tty_non_workspace_directory() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir().path();
temp_dir.join("main.ts").write("const a = 1;\n");
context.new_command().arg("fmt").with_pty(|mut pty| {
pty.expect("Are you sure you want to format the entire");
pty.write_raw("y\r\n");
pty.expect("Checked 1 file");
});

context.new_command().arg("fmt").with_pty(|mut pty| {
pty.expect("Are you sure you want to format");
pty.write_raw("n\r\n");
pty.expect("Did not format non-workspace directory");
});
}
1 change: 1 addition & 0 deletions tests/integration/watcher_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ async fn fmt_watch_without_args_test() {
.current_dir(t.path())
.arg("fmt")
.arg("--watch")
.arg(".")
.piped_output()
.spawn()
.unwrap();
Expand Down
2 changes: 1 addition & 1 deletion tests/specs/fmt/astro_file_with_css_comment/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"tempDir": true,
"args": "fmt --unstable-component",
"args": "fmt --unstable-component .",
"output": "fmt.out"
}
2 changes: 1 addition & 1 deletion tests/specs/fmt/njk/__test__.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"tempDir": true,
"steps": [
{
"args": "fmt --unstable-component",
"args": "fmt --unstable-component .",
"output": "[WILDLINE]badly_formatted.njk\nChecked 1 file\n"
},
{
Expand Down
21 changes: 21 additions & 0 deletions tests/specs/fmt/non_workspace/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"tests": {
"bare": {
"args": "fmt",
"output": "error.out",
"exitCode": 1
},
"specify_dir": {
"args": "fmt .",
"output": "[WILDCARD]"
},
"check": {
"args": "fmt --check",
"output": "[WILDCARD]"
},
"arg": {
"args": "fmt main.ts",
"output": "[WILDCARD]"
}
}
}
1 change: 1 addition & 0 deletions tests/specs/fmt/non_workspace/error.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
error: Did not format non-workspace directory. Run again specifying the current directory (ex. `deno fmt .`)
Empty file.
4 changes: 2 additions & 2 deletions tests/specs/fmt/sql/__test__.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"tempDir": true,
"tests": {
"nothing": {
"args": "fmt",
"args": "fmt .",
"output": "Checked 7 files\n"
},
"flag": {
"args": "fmt --unstable-sql",
"args": "fmt --unstable-sql .",
"output": "[UNORDERED_START]\n[WILDLINE]badly_formatted.sql\n[WILDLINE]wrong_file_ignore.sql\n[UNORDERED_END]\nChecked 7 files\n"
},
"config_file": {
Expand Down
2 changes: 1 addition & 1 deletion tests/specs/fmt/strip_bom/__test__.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"output": "check.out",
"exitCode": 1
}, {
"args": "fmt",
"args": "fmt .",
"output": "format.out"
}, {
"args": "run -R --unstable-raw-imports verify.ts",
Expand Down
Loading
Loading