Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ windows = { version = "0.54.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_SystemInformation",
"Win32_System_Threading",
"Win32_UI_Controls",
"Win32_UI_Controls_Dialogs",
Expand Down
11 changes: 11 additions & 0 deletions src/editor/scintilla.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const SCI_SETCODEPAGE: u32 = 2037;
const SCI_SETTEXT: u32 = 2181;
const SCI_GETTEXT: u32 = 2182;
const SCI_INSERTTEXT: u32 = 2003;
const SCI_REPLACESEL: u32 = 2170;
const SCI_GETLENGTH: u32 = 2006;
const SCI_GETCHARAT: u32 = 2007;
const SCI_GETCURRENTPOS: u32 = 2008;
Expand Down Expand Up @@ -673,6 +674,16 @@ pub fn insert_text(hwnd: HWND, pos: usize, text: &str) {
send_message(hwnd, SCI_INSERTTEXT, pos, text.as_ptr() as isize);
}

/// Replaces the current selection (or inserts at the caret if the selection
/// is empty) with `text`, then leaves the caret positioned after it — the
/// same behavior as typing or pasting.
pub fn replace_selection(hwnd: HWND, text: &str) {
let Ok(text) = CString::new(text) else {
return;
};
send_message(hwnd, SCI_REPLACESEL, 0, text.as_ptr() as isize);
}

/// The newline sequence matching the document's current EOL mode.
pub fn eol_string(hwnd: HWND) -> &'static str {
match get_eol_mode(hwnd) {
Expand Down
58 changes: 38 additions & 20 deletions src/platform/win32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use windows::Win32::Graphics::Gdi::{
use windows::Win32::System::Com::CoTaskMemFree;
use windows::Win32::System::DataExchange::COPYDATASTRUCT;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::SystemInformation::GetLocalTime;
use windows::Win32::UI::Controls::Dialogs::{
CommDlgExtendedError, GetOpenFileNameW, GetSaveFileNameW, OFN_EXPLORER, OFN_FILEMUSTEXIST,
OFN_OVERWRITEPROMPT, OFN_PATHMUSTEXIST, OPENFILENAMEW,
Expand Down Expand Up @@ -128,6 +129,7 @@ const IDM_EDIT_REPLACE: u16 = 323;
const IDM_EDIT_REPLACE_ALL: u16 = 324;
const IDM_EDIT_FIND_IN_FILES: u16 = 325;
const IDM_EDIT_GOTO_LINE: u16 = 332;
const IDM_EDIT_INSERT_DATETIME: u16 = 333;
const CMD_TRANSFORM_UPPERCASE: u16 = 326;
const CMD_TRANSFORM_LOWERCASE: u16 = 327;
const CMD_COPY_FULL_PATH: u16 = 328;
Expand Down Expand Up @@ -209,6 +211,7 @@ const VK_X: u16 = 0x58;
const VK_Y: u16 = 0x59;
const VK_Z: u16 = 0x5A;
const VK_F3: u16 = 0x72;
const VK_F5: u16 = 0x74;
const VK_N: u16 = 0x4E;
const VK_S: u16 = 0x53;
const VK_OEM_4: u16 = 0xDB;
Expand Down Expand Up @@ -433,7 +436,6 @@ struct AppState {
session_snapshot_periodic_backup: bool,
backup_interval_seconds: u32,
word_wrap_enabled: bool,
next_untitled_index: usize,
search_state: SearchState,
search_dialog_mode: SearchDialogMode,
find_dialog: Option<FindDialogState>,
Expand Down Expand Up @@ -839,6 +841,13 @@ fn create_menu() -> Result<HMENU> {
CMD_TRIM_LEADING_TRAILING as usize,
w!("Trim Leading + Trailing Whitespace"),
)?;
AppendMenuW(edit_menu, MF_SEPARATOR, 0, PCWSTR::null())?;
AppendMenuW(
edit_menu,
MF_STRING,
IDM_EDIT_INSERT_DATETIME as usize,
w!("Insert Date/Time"),
)?;
AppendMenuW(menu, MF_POPUP, edit_menu.0 as usize, w!("Edit"))?;

let view_menu = CreatePopupMenu()?;
Expand Down Expand Up @@ -1116,6 +1125,11 @@ fn create_accelerators() -> Result<HACCEL> {
key: VK_G,
cmd: IDM_EDIT_GOTO_LINE,
},
ACCEL {
fVirt: FVIRTKEY,
key: VK_F5,
cmd: IDM_EDIT_INSERT_DATETIME,
},
ACCEL {
fVirt: FVIRTKEY | FCONTROL,
key: VK_OEM_PLUS,
Expand Down Expand Up @@ -1553,6 +1567,14 @@ unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam:
}
LRESULT(0)
}
IDM_EDIT_INSERT_DATETIME => {
if let Some(state) = get_state(hwnd)
&& let Some(editor) = active_editor(state)
{
scintilla::replace_selection(editor, &current_date_time_stamp());
}
LRESULT(0)
}
IDM_EDIT_REDO => {
if let Some(state) = get_state(hwnd)
&& let Some(editor) = active_editor(state)
Expand Down Expand Up @@ -2430,7 +2452,6 @@ fn create_children(hwnd: HWND, instance: HINSTANCE) -> Result<AppState> {
session_snapshot_periodic_backup: session::DEFAULT_SESSION_SNAPSHOT_PERIODIC_BACKUP,
backup_interval_seconds: session::DEFAULT_BACKUP_INTERVAL_SECONDS,
word_wrap_enabled: session::DEFAULT_WORD_WRAP_ENABLED,
next_untitled_index: 1,
search_state: SearchState {
find_text: String::new(),
replace_text: String::new(),
Expand Down Expand Up @@ -4056,6 +4077,15 @@ fn count_words(text: &str) -> usize {
count
}

/// Current local date/time as "YYYY/MM/DD HH:MM", for Insert Date/Time.
fn current_date_time_stamp() -> String {
let st = unsafe { GetLocalTime() };
format!(
"{:04}-{:02}-{:02} {:02}:{:02}",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute
)
}

/// Formats a count with comma separators, e.g. 1234567 -> "1,234,567".
fn format_thousands(value: usize) -> String {
let digits = value.to_string();
Expand Down Expand Up @@ -5653,9 +5683,6 @@ fn restore_session_entry(
smart_highlight_truncated: false,
lexer_override: None,
};
if doc_tab.doc.path.is_none() {
update_next_untitled_index_from_name(state, &doc_tab.doc.display_name);
}
apply_syntax_for_doc(&doc_tab, state.editor_dark);
restore_strike_ranges(editor, &entry.strike_ranges);
let index = add_tab(state, &tab_title(&doc_tab), doc_tab)?;
Expand Down Expand Up @@ -5794,21 +5821,12 @@ fn backup_interval_ms(interval_secs: u32) -> u32 {
interval_secs.max(1).saturating_mul(1000)
}

fn next_untitled_name(state: &mut AppState) -> String {
let value = state.next_untitled_index;
state.next_untitled_index = state.next_untitled_index.saturating_add(1);
format!("new {value:03}")
}

fn update_next_untitled_index_from_name(state: &mut AppState, name: &str) {
if let Some(value) = name.strip_prefix("new ")
&& let Ok(parsed) = value.trim().parse::<usize>()
{
let candidate = parsed.saturating_add(1);
if candidate > state.next_untitled_index {
state.next_untitled_index = candidate;
}
}
fn next_untitled_name(_state: &mut AppState) -> String {
let st = unsafe { GetLocalTime() };
format!(
"{:04}-{:02}-{:02}-{:02}-{:02}",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute
)
}

fn ensure_doc_backup_path(doc: &mut Document) -> Result<()> {
Expand Down