mirror of
https://github.com/microsoft/edit.git
synced 2026-06-13 21:45:49 -05:00
545 lines
17 KiB
Rust
545 lines
17 KiB
Rust
use crate::helpers::{CoordType, Size};
|
|
use crate::{apperr, helpers};
|
|
use std::ffi::{CStr, OsString};
|
|
use std::fmt::Write as _;
|
|
use std::fs::{self, File};
|
|
use std::mem::MaybeUninit;
|
|
use std::os::windows::io::FromRawHandle;
|
|
use std::path::{Path, PathBuf};
|
|
use std::ptr::{null, null_mut};
|
|
use std::{mem, time};
|
|
use windows_sys::Win32::Foundation;
|
|
use windows_sys::Win32::Globalization;
|
|
use windows_sys::Win32::Storage::FileSystem;
|
|
use windows_sys::Win32::System::Console;
|
|
use windows_sys::Win32::System::Diagnostics::Debug;
|
|
use windows_sys::Win32::System::IO;
|
|
use windows_sys::Win32::System::LibraryLoader;
|
|
use windows_sys::Win32::System::Memory;
|
|
use windows_sys::Win32::System::Threading;
|
|
use windows_sys::w;
|
|
|
|
type ReadConsoleInputExW = unsafe extern "system" fn(
|
|
h_console_input: Foundation::HANDLE,
|
|
lp_buffer: *mut Console::INPUT_RECORD,
|
|
n_length: u32,
|
|
lp_number_of_events_read: *mut u32,
|
|
w_flags: u16,
|
|
) -> Foundation::BOOL;
|
|
|
|
unsafe extern "system" fn read_console_input_ex_placeholder(
|
|
_: Foundation::HANDLE,
|
|
_: *mut Console::INPUT_RECORD,
|
|
_: u32,
|
|
_: *mut u32,
|
|
_: u16,
|
|
) -> Foundation::BOOL {
|
|
panic!();
|
|
}
|
|
|
|
const CONSOLE_READ_NOWAIT: u16 = 0x0002;
|
|
|
|
struct State {
|
|
read_console_input_ex: ReadConsoleInputExW,
|
|
stdin: Foundation::HANDLE,
|
|
stdout: Foundation::HANDLE,
|
|
stdin_cp_old: u32,
|
|
stdout_cp_old: u32,
|
|
stdin_mode_old: u32,
|
|
stdout_mode_old: u32,
|
|
leading_surrogate: u16,
|
|
inject_resize: bool,
|
|
wants_exit: bool,
|
|
}
|
|
|
|
static mut STATE: State = State {
|
|
read_console_input_ex: read_console_input_ex_placeholder,
|
|
stdin: null_mut(),
|
|
stdout: null_mut(),
|
|
stdin_cp_old: 0,
|
|
stdout_cp_old: 0,
|
|
stdin_mode_old: 0,
|
|
stdout_mode_old: 0,
|
|
leading_surrogate: 0,
|
|
inject_resize: false,
|
|
wants_exit: false,
|
|
};
|
|
|
|
extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
|
|
unsafe {
|
|
STATE.wants_exit = true;
|
|
IO::CancelIoEx(STATE.stdin, null());
|
|
}
|
|
1
|
|
}
|
|
|
|
pub fn init() -> apperr::Result<()> {
|
|
unsafe {
|
|
let kernel32 = LibraryLoader::GetModuleHandleW(w!("kernel32.dll"));
|
|
STATE.read_console_input_ex = get_proc_address(kernel32, c"ReadConsoleInputExW")?;
|
|
|
|
check_bool_return(Console::SetConsoleCtrlHandler(
|
|
Some(console_ctrl_handler),
|
|
1,
|
|
))?;
|
|
|
|
STATE.stdin = FileSystem::CreateFileW(
|
|
w!("CONIN$"),
|
|
Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
|
|
FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
|
|
null_mut(),
|
|
FileSystem::OPEN_EXISTING,
|
|
0,
|
|
null_mut(),
|
|
);
|
|
STATE.stdout = FileSystem::CreateFileW(
|
|
w!("CONOUT$"),
|
|
Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
|
|
FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
|
|
null_mut(),
|
|
FileSystem::OPEN_EXISTING,
|
|
0,
|
|
null_mut(),
|
|
);
|
|
if STATE.stdin == Foundation::INVALID_HANDLE_VALUE
|
|
|| STATE.stdout == Foundation::INVALID_HANDLE_VALUE
|
|
{
|
|
return Err(get_last_error());
|
|
}
|
|
|
|
STATE.stdin_cp_old = Console::GetConsoleCP();
|
|
STATE.stdout_cp_old = Console::GetConsoleOutputCP();
|
|
check_bool_return(Console::GetConsoleMode(
|
|
STATE.stdin,
|
|
&raw mut STATE.stdin_mode_old,
|
|
))?;
|
|
check_bool_return(Console::GetConsoleMode(
|
|
STATE.stdout,
|
|
&raw mut STATE.stdout_mode_old,
|
|
))?;
|
|
|
|
check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
|
|
check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
|
|
check_bool_return(Console::SetConsoleMode(
|
|
STATE.stdin,
|
|
Console::ENABLE_WINDOW_INPUT
|
|
| Console::ENABLE_EXTENDED_FLAGS
|
|
| Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
|
|
))?;
|
|
check_bool_return(Console::SetConsoleMode(
|
|
STATE.stdout,
|
|
Console::ENABLE_PROCESSED_OUTPUT
|
|
| Console::ENABLE_WRAP_AT_EOL_OUTPUT
|
|
| Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
| Console::DISABLE_NEWLINE_AUTO_RETURN,
|
|
))?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn deinit() {
|
|
unsafe {
|
|
Console::SetConsoleCP(STATE.stdin_cp_old);
|
|
Console::SetConsoleOutputCP(STATE.stdout_cp_old);
|
|
Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old);
|
|
Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old);
|
|
}
|
|
}
|
|
|
|
pub fn inject_window_size_into_stdin() {
|
|
unsafe {
|
|
STATE.inject_resize = true;
|
|
}
|
|
}
|
|
|
|
fn get_console_size() -> Option<Size> {
|
|
unsafe {
|
|
let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed();
|
|
info.cbSize = mem::size_of::<Console::CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
|
|
if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 {
|
|
return None;
|
|
}
|
|
|
|
let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType;
|
|
let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType;
|
|
Some(Size {
|
|
width: w,
|
|
height: h,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Reads from stdin.
|
|
///
|
|
/// Returns `None` if there was an error reading from stdin.
|
|
/// Returns `Some("")` if the given timeout was reached.
|
|
/// Otherwise, it returns the read, non-empty string.
|
|
pub fn read_stdin(timeout: time::Duration) -> Option<String> {
|
|
let mut input_buf = [const { MaybeUninit::<Console::INPUT_RECORD>::uninit() }; 1024];
|
|
let mut input_buf_cap = input_buf.len();
|
|
let mut utf16_buf = [const { MaybeUninit::<u16>::uninit() }; 1024];
|
|
let mut utf16_buf_len = 0;
|
|
let mut resize_event = None;
|
|
let mut read_more = true;
|
|
let mut read_poll = false;
|
|
|
|
if unsafe { STATE.inject_resize } {
|
|
resize_event = get_console_size();
|
|
read_poll = true;
|
|
unsafe { STATE.inject_resize = false };
|
|
}
|
|
|
|
if timeout != time::Duration::MAX {
|
|
read_poll = true;
|
|
let wait_result =
|
|
unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) };
|
|
match wait_result {
|
|
// Ready to read? Continue with reading below.
|
|
// `read_more` is already true to ensure we don't block.
|
|
Foundation::WAIT_OBJECT_0 => {}
|
|
// Timeout? Skip reading entirely.
|
|
Foundation::WAIT_TIMEOUT => read_more = false,
|
|
// Error? Tell the caller stdin is broken.
|
|
_ => return None,
|
|
}
|
|
}
|
|
|
|
// This loops exists, just in case there's events in the input buffer that we aren't interested in.
|
|
// It should be rare for this to loop.
|
|
while read_more {
|
|
if unsafe { STATE.leading_surrogate } != 0 {
|
|
utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate });
|
|
utf16_buf_len = 1;
|
|
input_buf_cap -= 1;
|
|
unsafe { STATE.leading_surrogate = 0 };
|
|
}
|
|
|
|
let input = unsafe {
|
|
// If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup,
|
|
// but are still interested in any other pending input that may be waiting for us.
|
|
let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 };
|
|
let mut read = 0;
|
|
let ok = (STATE.read_console_input_ex)(
|
|
STATE.stdin,
|
|
input_buf[0].as_mut_ptr(),
|
|
input_buf_cap as u32,
|
|
&mut read,
|
|
flags,
|
|
);
|
|
if ok == 0 || STATE.wants_exit {
|
|
return None;
|
|
}
|
|
&*(&input_buf[..read as usize] as *const _ as *const [Console::INPUT_RECORD])
|
|
};
|
|
|
|
for inp in input {
|
|
match inp.EventType as u32 {
|
|
Console::KEY_EVENT => {
|
|
let event = unsafe { &inp.Event.KeyEvent };
|
|
let ch = unsafe { event.uChar.UnicodeChar };
|
|
if event.bKeyDown != 0 && ch != 0 {
|
|
utf16_buf[utf16_buf_len] = MaybeUninit::new(ch);
|
|
utf16_buf_len += 1;
|
|
}
|
|
}
|
|
Console::WINDOW_BUFFER_SIZE_EVENT => {
|
|
let event = unsafe { &inp.Event.WindowBufferSizeEvent };
|
|
let w = event.dwSize.X as CoordType;
|
|
let h = event.dwSize.Y as CoordType;
|
|
// Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s.
|
|
// E.g. starting conhost will emit 3 in a row. Skip rendering in that case.
|
|
if w > 0 && h > 0 {
|
|
resize_event = Some(Size {
|
|
width: w,
|
|
height: h,
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
read_more = !resize_event.is_some() && utf16_buf_len == 0;
|
|
}
|
|
|
|
const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t"
|
|
let resize_event_len = if resize_event.is_some() {
|
|
RESIZE_EVENT_FMT_MAX_LEN
|
|
} else {
|
|
0
|
|
};
|
|
// +1 to account for a potential `STATE.leading_surrogate`.
|
|
let utf8_max_len = (utf16_buf_len + 1) * 3;
|
|
let mut text = String::with_capacity(utf8_max_len + resize_event_len);
|
|
|
|
if let Some(resize_event) = resize_event {
|
|
// If I read xterm's documentation correctly, CSI 18 t reports the window size in characters.
|
|
// CSI 8 ; height ; width t is the response. Of course, we didn't send the request,
|
|
// but we can use this fake response to trigger the editor to resize itself.
|
|
_ = write!(
|
|
text,
|
|
"\x1b[8;{};{}t",
|
|
resize_event.height, resize_event.width
|
|
);
|
|
}
|
|
|
|
// If the input ends with a lone lead surrogate, we need to remember it for the next read.
|
|
if utf16_buf_len > 0 {
|
|
unsafe {
|
|
let last_char = utf16_buf[utf16_buf_len - 1].assume_init();
|
|
if 0xD800 <= last_char && last_char <= 0xDBFF {
|
|
STATE.leading_surrogate = last_char;
|
|
utf16_buf_len -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert the remaining input to UTF8, the sane encoding.
|
|
if utf16_buf_len > 0 {
|
|
unsafe {
|
|
let vec = text.as_mut_vec();
|
|
let spare = vec.spare_capacity_mut();
|
|
|
|
let len = Globalization::WideCharToMultiByte(
|
|
Globalization::CP_UTF8,
|
|
0,
|
|
utf16_buf[0].as_ptr(),
|
|
utf16_buf_len as i32,
|
|
spare.as_mut_ptr() as *mut _,
|
|
spare.len() as i32,
|
|
null(),
|
|
null_mut(),
|
|
);
|
|
|
|
if len > 0 {
|
|
vec.set_len(vec.len() + len as usize);
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(text)
|
|
}
|
|
|
|
pub fn write_stdout(text: &str) {
|
|
unsafe {
|
|
let mut offset = 0;
|
|
|
|
while offset < text.len() {
|
|
let ptr = text.as_ptr().add(offset);
|
|
let write = (text.len() - offset).min(1024 * 1024 * 1024) as u32;
|
|
let mut written = 0;
|
|
let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut());
|
|
offset += written as usize;
|
|
if ok == 0 || written == 0 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn open_stdin_if_redirected() -> Option<File> {
|
|
unsafe {
|
|
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
|
|
match FileSystem::GetFileType(handle) {
|
|
FileSystem::FILE_TYPE_DISK | FileSystem::FILE_TYPE_PIPE => {
|
|
Some(File::from_raw_handle(handle))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn canonicalize<P: AsRef<Path>>(path: P) -> apperr::Result<PathBuf> {
|
|
let mut path = fs::canonicalize(path)?;
|
|
let path = path.as_mut_os_string();
|
|
let mut path = mem::take(path).into_encoded_bytes();
|
|
|
|
if path.len() > 6
|
|
&& &path[0..4] == br"\\?\"
|
|
&& (b'A'..b'Z').contains(&path[4])
|
|
&& path[5] == b':'
|
|
{
|
|
path.drain(0..4);
|
|
}
|
|
|
|
let path = unsafe { OsString::from_encoded_bytes_unchecked(path) };
|
|
let path = PathBuf::from(path);
|
|
Ok(path)
|
|
}
|
|
|
|
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<*mut u8> {
|
|
unsafe {
|
|
let mut base = null_mut();
|
|
|
|
if cfg!(debug_assertions) {
|
|
static mut S_BASE_GEN: usize = 0x0000100000000000;
|
|
S_BASE_GEN += 0x0000100000000000;
|
|
base = S_BASE_GEN as *mut _;
|
|
}
|
|
|
|
check_ptr_return(Memory::VirtualAlloc(
|
|
base,
|
|
size,
|
|
Memory::MEM_RESERVE,
|
|
Memory::PAGE_READWRITE,
|
|
) as *mut u8)
|
|
}
|
|
}
|
|
|
|
pub unsafe fn virtual_release(base: *mut u8, size: usize) {
|
|
unsafe {
|
|
Memory::VirtualFree(base as *mut _, size, Memory::MEM_RELEASE);
|
|
}
|
|
}
|
|
|
|
pub unsafe fn virtual_commit(base: *mut u8, size: usize) -> apperr::Result<()> {
|
|
unsafe {
|
|
check_ptr_return(Memory::VirtualAlloc(
|
|
base as *mut _,
|
|
size,
|
|
Memory::MEM_COMMIT,
|
|
Memory::PAGE_READWRITE,
|
|
))
|
|
.map(|_| ())
|
|
}
|
|
}
|
|
|
|
unsafe fn load_library(name: *const u16) -> apperr::Result<Foundation::HMODULE> {
|
|
unsafe {
|
|
check_ptr_return(LibraryLoader::LoadLibraryExW(
|
|
name,
|
|
null_mut(),
|
|
LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32,
|
|
))
|
|
}
|
|
}
|
|
|
|
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
|
|
pub unsafe fn get_proc_address<T>(handle: Foundation::HMODULE, name: &CStr) -> apperr::Result<T> {
|
|
unsafe {
|
|
let ptr = LibraryLoader::GetProcAddress(handle, name.as_ptr() as *const u8);
|
|
if let Some(ptr) = ptr {
|
|
Ok(mem::transmute_copy(&ptr))
|
|
} else {
|
|
Err(get_last_error())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub unsafe fn load_icu() -> apperr::Result<Foundation::HMODULE> {
|
|
unsafe { load_library(w!("icu.dll")) }
|
|
}
|
|
|
|
pub fn preferred_languages() -> Vec<String> {
|
|
unsafe {
|
|
const LEN: usize = 256;
|
|
|
|
let mut lang_num = 0;
|
|
let mut lang_buf = [const { MaybeUninit::<u16>::uninit() }; LEN];
|
|
let mut lang_buf_len = lang_buf.len() as u32;
|
|
if Globalization::GetUserPreferredUILanguages(
|
|
Globalization::MUI_LANGUAGE_NAME,
|
|
&mut lang_num,
|
|
lang_buf[0].as_mut_ptr(),
|
|
&mut lang_buf_len,
|
|
) == 0
|
|
|| lang_num == 0
|
|
{
|
|
return Vec::new();
|
|
}
|
|
|
|
// Drop the terminating double-null character.
|
|
lang_buf_len = lang_buf_len.saturating_sub(1);
|
|
|
|
let mut lang_buf_utf8 = [const { MaybeUninit::<u8>::uninit() }; 3 * LEN];
|
|
let lang_buf_utf8_len = Globalization::WideCharToMultiByte(
|
|
Globalization::CP_UTF8,
|
|
0,
|
|
lang_buf[0].as_mut_ptr(),
|
|
lang_buf_len as i32,
|
|
lang_buf_utf8[0].as_mut_ptr(),
|
|
lang_buf_utf8.len() as i32,
|
|
null(),
|
|
null_mut(),
|
|
);
|
|
if lang_buf_utf8_len == 0 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let result = helpers::str_from_raw_parts_mut(
|
|
lang_buf_utf8[0].as_mut_ptr(),
|
|
lang_buf_utf8_len as usize,
|
|
);
|
|
result.make_ascii_lowercase();
|
|
result.split_terminator('\0').map(String::from).collect()
|
|
}
|
|
}
|
|
|
|
#[cold]
|
|
fn get_last_error() -> apperr::Error {
|
|
unsafe { gle_to_apperr(Foundation::GetLastError()) }
|
|
}
|
|
|
|
#[inline]
|
|
fn gle_to_apperr(gle: u32) -> apperr::Error {
|
|
unsafe {
|
|
apperr::Error::new(if gle == 0 {
|
|
0x8000FFFF
|
|
} else {
|
|
0x80070000 | gle
|
|
})
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
|
|
gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32)
|
|
}
|
|
|
|
pub fn format_error(err: apperr::Error) -> String {
|
|
unsafe {
|
|
let mut ptr: *mut u8 = null_mut();
|
|
let len = Debug::FormatMessageA(
|
|
Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER
|
|
| Debug::FORMAT_MESSAGE_FROM_SYSTEM
|
|
| Debug::FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
null(),
|
|
err.value() as u32,
|
|
0,
|
|
&mut ptr as *mut *mut _ as *mut _,
|
|
0,
|
|
null_mut(),
|
|
);
|
|
|
|
let mut result = format!("Error {:#08x}", err.value());
|
|
|
|
if len > 0 {
|
|
let msg = helpers::str_from_raw_parts(ptr, len as usize);
|
|
let msg = msg.trim_ascii();
|
|
let msg = msg.replace(['\r', '\n'], " ");
|
|
result.push_str(": ");
|
|
result.push_str(&msg);
|
|
Foundation::LocalFree(ptr as *mut _);
|
|
}
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> {
|
|
if ret == 0 {
|
|
Err(get_last_error())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn check_ptr_return<T>(ret: *mut T) -> apperr::Result<*mut T> {
|
|
if ret.is_null() {
|
|
Err(get_last_error())
|
|
} else {
|
|
Ok(ret)
|
|
}
|
|
}
|