/* FUTURE FEATURES: - more tests - prepare rename support - custom file/build command associations - config variables - bind key to series of commands - convert macro to command list - plugins? - built-in plugins: - "remove file..." menu - auto-close brackets - with macros we can really test performance of buffer_insert_text_at_pos, etc. (which should ideally be fast) - better manual - restart LSP server automatically? - LSP request timeout - reflow command - editing files with invalid UTF-8 */ /* macros defining ted data directory locations. the first character can be interpreted specially if it is one of the following: ~ home directory ^ user's AppData\Local directory (Windows only) @ directory containing ted executable */ #ifndef TED_GLOBAL_DATA_DIR #if _WIN32 #define TED_GLOBAL_DATA_DIR "@" #else #define TED_GLOBAL_DATA_DIR "/usr/share/ted" #endif #endif #ifndef TED_LOCAL_DATA_DIR #if _WIN32 #define TED_LOCAL_DATA_DIR "^/ted" #else #define TED_LOCAL_DATA_DIR "~/.local/share/ted" #endif #endif #include "ted-internal.h" #include #include #if __linux__ #include #endif #if __unix__ #include #endif #if _WIN32 #include #pragma comment(lib, "dbghelp.lib") #pragma comment(lib, "ole32.lib") #pragma comment(lib, "opengl32.lib") #pragma comment(lib, "shell32.lib") #pragma comment(lib, "ws2_32.lib") #endif #if !defined ONE_SOURCE && !defined DEBUG #define ONE_SOURCE 1 #endif #if PROFILE #define PROFILE_FRAME 1 #endif #if ONE_SOURCE #include "util.c" #if _WIN32 #include "os-win.c" #elif __unix__ #include "os-posix.c" #else #error "Unrecognized operating system." #endif #include "gl.c" #include "text.c" #include "colors.c" #include "syntax.c" #include "buffer.c" #include "ted.c" #include "ui.c" #include "find.c" #include "node.c" #include "build.c" #include "tags.c" #include "menu.c" #include "ide-autocomplete.c" #include "ide-signature-help.c" #include "ide-rename-symbol.c" #include "ide-hover.c" #include "ide-definitions.c" #include "ide-highlights.c" #include "ide-usages.c" #include "ide-document-link.c" #include "ide-format.c" #include "command.c" #include "macro.c" #include "config.c" #include "session.c" #include "lsp.c" #include "lsp-json.c" #include "lsp-write.c" #include "lsp-parse.c" #endif // ONE_SOURCE static Rect message_box_rect(Ted *ted) { Font *font = ted->font; const Settings *settings = ted_active_settings(ted); float padding = settings->padding; float window_width = ted->window_width, window_height = ted->window_height; float char_height = text_font_char_height(font); return rect_centered((vec2){window_width * 0.5f, window_height * 0.9f}, (vec2){ted_get_menu_width(ted), 3 * char_height + 2 * padding}); } #if DEBUG static void APIENTRY gl_message_callback(GLenum source, GLenum type, unsigned int id, GLenum severity, GLsizei length, const char *message, const void *userParam) { (void)source; (void)type; (void)id; (void)length; (void)userParam; if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return; debug_println("Message from OpenGL: %s.", message); } #endif #define CRASH_CRASH_MESSAGE "ted crashed while trying to handle a crash! yikes! ):" #define CRASH_MESSAGE "ted has crashed ): Please send %s/log.txt to pommicket""@gmail.com if you want this fixed.", ted->local_data_dir #define CRASH_STARTUP_MESSAGE "ted crashed when starting up ):" static Ted *error_signal_handler_ted; static bool signal_being_handled; // prevent infinite signal recursion #if __unix__ static void error_signal_handler(int signum, siginfo_t *info, void *context) { (void)context; if (signal_being_handled) die(CRASH_CRASH_MESSAGE); signal_being_handled = true; Ted *ted = error_signal_handler_ted; if (ted) { FILE *log = ted->log; if (log) { fprintf(log, "Signal %d: %s\n", signum, strsignal(signum)); fprintf(log, "errno = %d\n", info->si_errno); fprintf(log, "code = %d\n", info->si_code); fprintf(log, "address = 0x%llx\n", (unsigned long long)info->si_addr); #if __linux__ fprintf(log, "utime = %lu\n", (unsigned long)info->si_utime); fprintf(log, "stime = %lu\n", (unsigned long)info->si_stime); fprintf(log, "address lsb = %d\n", info->si_addr_lsb); fprintf(log, "lower bound = 0x%llx\n", (unsigned long long)info->si_lower); fprintf(log, "lower bound = 0x%llx\n", (unsigned long long)info->si_upper); fprintf(log, "syscall address = 0x%llx\n", (unsigned long long)info->si_call_addr); fprintf(log, "syscall = 0x%d\n", info->si_syscall); fprintf(log, "Backtrace:\n"); void *addresses[300] = {0}; int n_addresses = backtrace(addresses, (int)arr_count(addresses)); for (int i = 0; i < n_addresses; ++i) { fprintf(log, " 0x%llx\n", (unsigned long long)addresses[i]); } #endif fclose(log); } die(CRASH_MESSAGE); } else { die(CRASH_STARTUP_MESSAGE); } } #elif _WIN32 static const char *windows_exception_to_str(DWORD exception_code) { switch (exception_code) { case EXCEPTION_ACCESS_VIOLATION: return "Access violation"; case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "Array out of bounds"; case EXCEPTION_BREAKPOINT: return "Breakpoint"; case EXCEPTION_DATATYPE_MISALIGNMENT: return "Misaligned read or write"; case EXCEPTION_FLT_DENORMAL_OPERAND: return "Floating-point denormal operand"; case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "Floating-point division by zero"; case EXCEPTION_FLT_INEXACT_RESULT: return "Floating-point inexact result"; case EXCEPTION_FLT_INVALID_OPERATION: return "Floating-point invalid operation"; case EXCEPTION_FLT_OVERFLOW: return "Floating-point overflow"; case EXCEPTION_FLT_STACK_CHECK: return "Floating-point stack over/underflow"; case EXCEPTION_FLT_UNDERFLOW: return "Floating-point underflow"; case EXCEPTION_ILLEGAL_INSTRUCTION: return "Illegal instruction"; case EXCEPTION_IN_PAGE_ERROR: return "Page not present"; case EXCEPTION_INT_DIVIDE_BY_ZERO: return "Integer divide by zero"; case EXCEPTION_INT_OVERFLOW: return "Integer overflow"; case EXCEPTION_INVALID_DISPOSITION: return "Invalid disposition"; case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "Continue after non-continuable exception"; case EXCEPTION_PRIV_INSTRUCTION: return "Private instruction executed"; case EXCEPTION_SINGLE_STEP: return "Single step"; case EXCEPTION_STACK_OVERFLOW: return "Stack overflow"; } return "Unknown exception"; } static LONG WINAPI error_signal_handler(EXCEPTION_POINTERS *info) { if (signal_being_handled) die(CRASH_CRASH_MESSAGE); signal_being_handled = true; Ted *ted = error_signal_handler_ted; if (ted) { FILE *log = ted->log; if (log) { DWORD exception_code = info->ExceptionRecord->ExceptionCode; fprintf(log, "Exception 0x%lx: %s.\n", (unsigned long)exception_code, windows_exception_to_str(exception_code)); fprintf(log, "Address: 0x%llx.\n", (unsigned long long)info->ExceptionRecord->ExceptionAddress); fprintf(log, "Info0: 0x%llx.\n", (unsigned long long)info->ExceptionRecord->ExceptionInformation[0]); fprintf(log, "Info1: 0x%llx.\n", (unsigned long long)info->ExceptionRecord->ExceptionInformation[1]); fprintf(log, "Info2: 0x%llx.\n", (unsigned long long)info->ExceptionRecord->ExceptionInformation[2]); #if _M_AMD64 CONTEXT *context = info->ContextRecord; if (exception_code == EXCEPTION_STACK_OVERFLOW) { // don't backtrace; just output current address fprintf(log, "Instruction: 0x%llx\n", (unsigned long long)context->Rip); } else { fprintf(log, "Backtrace:\n"); HANDLE process = GetCurrentProcess(), thread = GetCurrentThread(); // backtrace // this here was very helpful: https://gist.github.com/jvranish/4441299 if (SymInitialize(process, NULL, true)) { STACKFRAME frame = {0}; frame.AddrPC.Offset = context->Rip; frame.AddrStack.Offset = context->Rsp; frame.AddrFrame.Offset = context->Rbp; frame.AddrPC.Mode = frame.AddrStack.Mode = frame.AddrFrame.Mode = AddrModeFlat; while (StackWalk(IMAGE_FILE_MACHINE_AMD64, process, thread, &frame, context, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL)) { fprintf(log, "0x%llx\n", (unsigned long long)frame.AddrPC.Offset); } SymCleanup(process); } } #endif fclose(log); } die(CRASH_MESSAGE); } else { die(CRASH_STARTUP_MESSAGE); } // MSVC is smart here and realizes this code is unreachable. // that said, I'm worried about not returning a value here for older MSVC versions // and just in general. #if _MSC_VER #pragma warning(push) #pragma warning(disable:4702) #endif return EXCEPTION_EXECUTE_HANDLER; #if _MSC_VER #pragma warning(pop) #endif } #endif static void ted_update_window_dimensions(Ted *ted) { int w = 0, h = 0; SDL_GetWindowSize(ted->window, &w, &h); gl_window_width = ted->window_width = (float)w; gl_window_height = ted->window_height = (float)h; } #if _WIN32 INT WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow) { (void)hInstance; (void)hPrevInstance; (void)lpCmdLine; (void)nCmdShow; int argc = 0; LPWSTR* wide_argv = CommandLineToArgvW(GetCommandLineW(), &argc); char** argv = calloc(argc + 1, sizeof *argv); if (!argv) { die("Out of memory."); } for (int i = 0; i < argc; i++) { LPWSTR wide_arg = wide_argv[i]; int len = (int)wcslen(wide_arg); int bufsz = len * 4 + 8; argv[i] = calloc((size_t)bufsz, 1); if (!argv[i]) die("Out of memory."); WideCharToMultiByte(CP_UTF8, 0, wide_arg, len, argv[i], bufsz - 1, NULL, NULL); } LocalFree(wide_argv); { WSADATA wsaData = {0}; WSAStartup(MAKEWORD(2, 2), &wsaData); } SetProcessDPIAware(); #else int main(int argc, char **argv) { #endif PROFILE_TIME(init_start) PROFILE_TIME(basic_init_start) #if __unix__ { struct sigaction act = {0}; act.sa_sigaction = error_signal_handler; act.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &act, NULL); sigaction(SIGFPE, &act, NULL); sigaction(SIGABRT, &act, NULL); sigaction(SIGILL, &act, NULL); signal(SIGPIPE, SIG_IGN); } #elif _WIN32 SetUnhandledExceptionFilter(error_signal_handler); #else #error "Unrecognized operating system." #endif #if _WIN32 setlocale(LC_ALL, ".65001"); #else setlocale(LC_ALL, "C.UTF-8"); #endif command_init(); color_init(); syntax_init(); // read command-line arguments int dash_dash = argc; for (int i = 1; i < argc; ++i) { if (streq(argv[i], "--")) { dash_dash = i; break; } } bool test = false; const char **starting_files = NULL; for (int i = 1; i < dash_dash; ++i) { if (streq(argv[i], "--help")) { printf("%s\n", TED_VERSION_FULL); printf("A text editor by pommicket.\n"); printf("For more information see https://github.com/pommicket/ted\n"); printf("\n"); printf("Usage: ted [--help] [--version] [--] [file names]\n"); exit(0); } else if (streq(argv[i], "--version")) { printf("%s\n", TED_VERSION_FULL); exit(0); } #if DEBUG else if (streq(argv[i], "--test")) { test = true; } #endif else if (argv[i][0] == '-') { fprintf(stderr, "Unrecognized option: %s\n", argv[i]); exit(EXIT_FAILURE); } else { arr_add(starting_files, argv[i]); } } if (!test) { for (int i = dash_dash + 1; i < argc; ++i) { arr_add(starting_files, argv[i]); } } PROFILE_TIME(basic_init_end) PROFILE_TIME(sdl_start) SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1"); // if this program is sent a SIGTERM/SIGINT, don't turn it into a quit event if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER) < 0) die("%s", SDL_GetError()); PROFILE_TIME(sdl_end) PROFILE_TIME(misc_start) Ted *ted = calloc(1, sizeof *ted); if (!ted) { die("Not enough memory available to run ted."); } ted->last_save_time = -1e50; ted->pid = process_get_id(); ted_update_time(ted); // make sure signal handler has access to ted. error_signal_handler_ted = ted; os_get_cwd(ted->start_cwd, sizeof ted->start_cwd); { // get local and global data directory #if _WIN32 char *appdata = NULL; wchar_t *appdata_wide = NULL; KNOWNFOLDERID id = FOLDERID_LocalAppData; if (SHGetKnownFolderPath(&id, 0, NULL, &appdata_wide) == S_OK) { size_t sz = wcslen(appdata_wide) * 4 + 1; appdata = malloc(sz); snprintf(appdata, sz, "%ls", appdata_wide); CoTaskMemFree(appdata_wide); appdata_wide = NULL; } id = FOLDERID_Profile; wchar_t *home_wide = NULL; if (SHGetKnownFolderPath(&id, 0, NULL, &home_wide) == S_OK) { strbuf_printf(ted->home, "%ls", home_wide); CoTaskMemFree(home_wide); } WCHAR executable_wide_path[TED_PATH_MAX] = {0}; char executable_dir[TED_PATH_MAX] = {0}; if (GetModuleFileNameW(NULL, executable_wide_path, sizeof executable_wide_path - 1) > 0) { WideCharToMultiByte(CP_UTF8, 0, executable_wide_path, -1, executable_dir, sizeof executable_dir, NULL, NULL); char *last_backslash = strrchr(executable_dir, '\\'); if (last_backslash) { *last_backslash = '\0'; } } #elif __unix__ char *home = getenv("HOME"); strbuf_printf(ted->home, "%s", home); char executable_dir[TED_PATH_MAX] = {0}; ssize_t len = readlink("/proc/self/exe", executable_dir, sizeof executable_dir - 1); if (len == -1) { // some posix systems don't have /proc/self/exe. oh well. } else { executable_dir[len] = '\0'; char *last_slash = strrchr(executable_dir, '/'); if (last_slash) { *last_slash = '\0'; // if we started in the directory where the executable is located, // we're probably debugging ted, so search the start cwd for ted.cfg, etc. ted->search_start_cwd = streq(ted->start_cwd, executable_dir); } } #else #error "unrecognized OS" #endif // replace special characters at start of data dirs typedef struct { const char *src; char *dest; size_t size; } DataDir; DataDir data_dirs[] = { {.src = TED_LOCAL_DATA_DIR, .dest = ted->local_data_dir, .size = sizeof ted->local_data_dir}, {.src = TED_GLOBAL_DATA_DIR, .dest = ted->global_data_dir, .size = sizeof ted->global_data_dir}, }; for (size_t i = 0; i < arr_count(data_dirs); i++) { const char *src = data_dirs[i].src; char *dest = data_dirs[i].dest; size_t size = data_dirs[i].size; if (!src[0] || !strchr(ALL_PATH_SEPARATORS, src[1])) goto absolute_path; switch (src[0]) { case '~': str_printf(dest, size, "%s%s", ted->home, src + 1); break; #if _WIN32 case '^': str_printf(dest, size, "%s%s", appdata, src + 1); break; #endif case '@': str_printf(dest, size, "%s%s", executable_dir, src + 1); break; default: absolute_path: if (!path_is_absolute(src)) { die("Data directory %s is not an absolute path", src); } str_cpy(dest, size, src); } // ensure we always use the same path separator for (int c = 0; dest[c]; c++) { if (strchr(ALL_PATH_SEPARATORS, dest[c])) dest[c] = PATH_SEPARATOR; } } if (fs_path_type(ted->global_data_dir) == FS_NON_EXISTENT) { die("Couldn't open ted data directory at %s", ted->global_data_dir); } if (fs_path_type(ted->local_data_dir) == FS_NON_EXISTENT) fs_mkdir(ted->local_data_dir); #if _WIN32 free(appdata); #endif } { // open log file char log_filename[TED_PATH_MAX]; char log1_filename[TED_PATH_MAX]; strbuf_printf(log_filename, "%s/log.txt", ted->local_data_dir); strbuf_printf(log1_filename, "%s/log.1.txt", ted->local_data_dir); if (fs_file_size(log_filename) > 500000) { remove(log1_filename); rename(log_filename, log1_filename); } ted->log = fopen(log_filename, "a"); ted_log(ted, "starting ted\n"); } { // get current working directory os_get_cwd(ted->cwd, sizeof ted->cwd); } #if TED_FORCE_SEARCH_START_CWD // override whether or not we are in the executable's directory // (for testing on Unix systems without /proc) ted->search_start_cwd = true; #endif PROFILE_TIME(misc_end) PROFILE_TIME(window_start) SDL_Window *window = SDL_CreateWindow("ted", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, (test ? SDL_WINDOW_HIDDEN : SDL_WINDOW_SHOWN)|SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE); if (!window) die("%s", SDL_GetError()); ted->window = window; { // set icon char icon_filename[TED_PATH_MAX]; if (ted_get_file(ted, "assets/icon.bmp", icon_filename, sizeof icon_filename)) { SDL_Surface *icon = SDL_LoadBMP(icon_filename); SDL_SetWindowIcon(window, icon); SDL_FreeSurface(icon); } // if we can't find the icon file, it's no big deal } PROFILE_TIME(window_end) PROFILE_TIME(gl_start) SDL_GLContext *glctx = NULL; { // get OpenGL context int gl_versions[][2] = { {4,3}, {3,0}, {2,0}, {0,0}, }; for (int i = 0; gl_versions[i][0]; ++i) { gl_version_major = gl_versions[i][0]; gl_version_minor = gl_versions[i][1]; SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl_version_major); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl_version_minor); #if DEBUG SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); #endif glctx = SDL_GL_CreateContext(window); if (glctx) { break; } else { debug_println("Couldn't get GL %d.%d context. Falling back to %d.%d.", gl_versions[i][0], gl_versions[i][1], gl_versions[i+1][0], gl_versions[i+1][1]); } } if (!glctx) die("%s", SDL_GetError()); gl_get_procs(); } #if DEBUG if (gl_version_major * 100 + gl_version_minor >= 403) { GLint flags = 0; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { // set up debug message callback glDebugMessageCallback(gl_message_callback, NULL); glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE); } } #endif ted->file_selector = file_selector_new(); gl_geometry_init(); text_init(); menu_init(ted); find_init(ted); macros_init(ted); definitions_init(ted); autocomplete_init(ted); format_init(ted); signature_help_init(ted); usages_init(ted); highlights_init(ted); hover_init(ted); rename_symbol_init(ted); document_link_init(ted); PROFILE_TIME(gl_end) PROFILE_TIME(configs_start) ted_load_configs(ted); PROFILE_TIME(configs_end) PROFILE_TIME(fonts_start) ted_load_fonts(ted); PROFILE_TIME(fonts_end) PROFILE_TIME(create_start) { TextBuffer *lbuffer = ted->line_buffer = line_buffer_new(ted); if (!lbuffer || buffer_has_error(lbuffer)) die("Error creating line buffer: %s", buffer_get_error(lbuffer)); } ted->find_buffer = line_buffer_new(ted); ted->replace_buffer = line_buffer_new(ted); ted->argument_buffer = line_buffer_new(ted); ted->build_buffer = buffer_new(ted); buffer_new_file(ted->build_buffer, NULL); for (u32 i = 0; i < arr_len(starting_files); ++i) { const char *filename = starting_files[i]; if (filename) { if (fs_file_exists(filename)) { ted_open_file(ted, filename); } else { ted_new_file(ted, filename); } } } if (!test && arr_len(starting_files) == 0) { session_read(ted); } arr_free(starting_files); ted->cursor_ibeam = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM); ted->cursor_wait = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_WAIT); ted->cursor_arrow = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW); ted->cursor_resize_h = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE); ted->cursor_resize_v = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENS); ted->cursor_hand = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND); ted->cursor_move = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL); PROFILE_TIME(create_end) PROFILE_TIME(get_ready_start) Uint32 time_at_last_frame = SDL_GetTicks(); SDL_GL_SetSwapInterval(1); // vsync PROFILE_TIME(get_ready_end) PROFILE_TIME(init_end) #if PROFILE print("Initialization: %.1fms\n", 1000 * (init_end - init_start)); print(" - Basic init: %.1fms\n", 1000 * (basic_init_end - basic_init_start)); print(" - SDL: %.1fms\n", 1000 * (sdl_end - sdl_start)); print(" - SDL window: %.1fms\n", 1000 * (window_end - window_start)); print(" - Create: %.1fms\n", 1000 * (create_end - create_start)); print(" - OpenGL: %.1fms\n", 1000 * (gl_end - gl_start)); print(" - misc: %.1fms\n", 1000 * (misc_end - misc_start)); print(" - Loading fonts: %.1fms\n", 1000 * (fonts_end - fonts_start)); print(" - Read configs: %.1fms\n", 1000 * (configs_end - configs_start)); print(" - Get ready: %.1fms\n", 1000 * (get_ready_end - get_ready_start)); #endif { // clear event queue // this is probably only a problem for me, but // some events that the WM should have consumed // are going to ted SDL_Event event; while (SDL_PollEvent(&event)); } double start_time = time_get_seconds(); double scroll_wheel_text_size_change = 0.0; while (!ted->quit) { ted_update_time(ted); double frame_start = ted->frame_time; SDL_PumpEvents(); u32 key_modifier = ted_get_key_modifier(ted); { // get mouse position int mouse_x = 0, mouse_y = 0; ted->mouse_state = SDL_GetMouseState(&mouse_x, &mouse_y); ted->mouse_pos = (vec2){(float)mouse_x, (float)mouse_y}; } for (size_t i = 0; i < arr_count(ted->mouse_clicks); ++i) arr_clear(ted->mouse_clicks[i]); for (size_t i = 0; i < arr_count(ted->mouse_releases); ++i) arr_clear(ted->mouse_releases[i]); ted->scroll_total_x = ted->scroll_total_y = 0; ted_update_window_dimensions(ted); { const Settings *active_settings = ted_active_settings(ted); // we don't properly handle variable-width fonts text_font_set_force_monospace(ted->font, active_settings->force_monospace); text_font_set_force_monospace(ted->font_bold, active_settings->force_monospace); } SDL_Event event; while (SDL_PollEvent(&event)) { TextBuffer *buffer = ted->active_buffer; switch (event.type) { case SDL_QUIT: command_execute(ted, CMD_QUIT, 1); break; case SDL_MOUSEWHEEL: { if (ted_is_ctrl_down(ted)) { // adjust text size with ctrl+scroll const Settings *settings = ted_active_settings(ted); scroll_wheel_text_size_change += settings->ctrl_scroll_adjust_text_size * event.wheel.preciseY; } else if (key_modifier == 0) { // scroll with mouse wheel Sint32 dx = event.wheel.x, dy = -event.wheel.y; if (autocomplete_box_contains_point(ted, ted_mouse_pos(ted))) { autocomplete_scroll(ted, dy); } else { ted->scroll_total_x += dx; ted->scroll_total_y += dy; } } } break; case SDL_MOUSEBUTTONDOWN: { if (ted->recording_macro) break; // ignore mouse input during macros Uint32 button = event.button.button; u8 times = event.button.clicks; // number of clicks float x = (float)event.button.x, y = (float)event.button.y; if (button == SDL_BUTTON_X1) { ted_press_key(ted, KEYCODE_X1, key_modifier); } else if (button == SDL_BUTTON_X2) { ted_press_key(ted, KEYCODE_X2, key_modifier); } if (button >= arr_count(ted->mouse_clicks)) break; vec2 pos = {x, y}; bool add = true; if (*ted->message_shown) { if (rect_contains_point(message_box_rect(ted), pos)) { // clicked on message if (button == SDL_BUTTON_LEFT) { // dismiss message *ted->message_shown = '\0'; } // don't let anyone else use this event add = false; } } if (add) { // handle mouse click // we need to do this here, and not in buffer_render, because ctrl+click (go to definition) // could switch to a different buffer. // line buffer click handling, IS done in buffer_render (yes this is less than ideal) if (!menu_is_any_open(ted)) { arr_foreach_ptr(ted->nodes, NodePtr, pnode) { Node *node = *pnode; TextBuffer *tab = node_get_tab(node, node_active_tab(node)); if (tab) { if (buffer_handle_click(ted, tab, pos, times)) { add = false; break; } } } if (add && ted->build_shown) if (buffer_handle_click(ted, ted->build_buffer, pos, times)) // handle build buffer clicks add = false; } } if (add) { MouseClick click = { .pos = pos, .times = times, }; arr_add(ted->mouse_clicks[button], click); } } break; case SDL_MOUSEBUTTONUP: { if (ted->recording_macro) break; // ignore mouse input during macros Uint8 button = event.button.button; if (button >= arr_count(ted->mouse_releases)) break; vec2 pos = {(float)event.button.x, (float)event.button.y}; MouseRelease release = { .pos = pos }; arr_add(ted->mouse_releases[button], release); } break; case SDL_MOUSEMOTION: { if (ted->recording_macro) break; // ignore mouse input during macros float x = (float)event.motion.x, y = (float)event.motion.y; if (ted->drag_buffer != ted->active_buffer) ted->drag_buffer = NULL; if (ted->drag_buffer) { BufferPos pos = {0}; // drag to select // we don't check the return value here, because it's okay to drag off the screen. buffer_pixels_to_pos(ted->drag_buffer, (vec2){x, y}, &pos); buffer_select_to_pos(ted->drag_buffer, pos); } hover_reset_timer(ted); } break; case SDL_KEYDOWN: { SDL_Keycode keycode = event.key.keysym.sym; SDL_Keymod modifier = event.key.keysym.mod; ted_press_key(ted, keycode, modifier); } break; case SDL_TEXTINPUT: { const char *text = event.text.text; if (buffer // unfortunately, some key combinations like ctrl+minus still register as a "-" text input event && (key_modifier & ~KEY_MODIFIER_SHIFT) == 0) { // insert the text command_execute_string_argument(ted, CMD_INSERT_TEXT, text); // check for trigger character LSP *lsp = buffer_lsp(buffer); const Settings *settings = buffer_settings(buffer); if (lsp && settings->trigger_characters) { u32 last_code_point = (u32)strlen(text) - 1; while (last_code_point > 0 && unicode_is_continuation_byte((u8)text[last_code_point])) --last_code_point; char32_t last_char = 0; unicode_utf8_to_utf32(&last_char, &text[last_code_point], strlen(text) - last_code_point); arr_foreach_ptr(lsp_completion_trigger_chars(lsp), const char32_t, c) { if (*c == last_char) { autocomplete_open(ted, last_char); break; } } // NOTE: we are not checking for signature help trigger // characters because currently we ask for signature // help any time a character is inserted. if (is32_word(last_char) && !is32_digit(last_char)) { if (settings->identifier_trigger_characters) { autocomplete_open(ted, last_char); } } } } } break; } } { int mx = 0, my = 0; ted->mouse_state = SDL_GetMouseState(&mx, &my); ted->mouse_pos = (vec2){(float)mx, (float)my}; } // default to arrow cursor ted->cursor = ted->cursor_arrow; if (!(ted->mouse_state & SDL_BUTTON_LMASK)) { // originally this was done on SDL_MOUSEBUTTONUP events but for some reason // I was getting a bunch of those even when I was holding down the mouse. // This makes it much smoother. ted->drag_buffer = NULL; } { // ted->cwd should be the directory containing the last active buffer TextBuffer *buffer = ted->active_buffer; if (buffer) { if (buffer_is_named_file(buffer)) { const char *buffer_path = buffer_get_path(buffer); assert(*buffer_path); char *last_sep = strrchr(buffer_path, PATH_SEPARATOR); if (last_sep) { size_t dirname_len = (size_t)(last_sep - buffer_path); if (dirname_len == 0) dirname_len = 1; // make sure "/x" sets our cwd to "/", not "" // set cwd to buffer's directory memcpy(ted->cwd, buffer_path, dirname_len); ted->cwd[dirname_len] = 0; } } } } // check if active buffer should be reloaded { TextBuffer *active_buffer = ted->active_buffer; if (active_buffer && buffer_externally_changed(active_buffer)) { if (buffer_settings(active_buffer)->auto_reload) buffer_reload(active_buffer); else { buffer_display_filename(active_buffer, ted->ask_reload, sizeof ted->ask_reload); menu_open(ted, MENU_ASK_RELOAD); } } } double frame_dt; { Uint32 time_this_frame = SDL_GetTicks(); frame_dt = 0.001 * (time_this_frame - time_at_last_frame); time_at_last_frame = time_this_frame; } { // when the user ctrl+scrolls, only actually change the text size // every 100ms, to avoid loading the font over and over again super fast static double last_font_adjust = 0; if (ted->frame_time - last_font_adjust > 0.1) { last_font_adjust = ted->frame_time; int dsize = (int)floor(scroll_wheel_text_size_change); command_execute(ted, CMD_TEXT_SIZE_INCREASE, dsize); scroll_wheel_text_size_change -= dsize; } } TextBuffer *active_buffer = ted->active_buffer; if (active_buffer && key_modifier == KEY_MODIFIER_ALT) { // alt + arrow keys to scroll double scroll_speed = 40.0; double scroll_amount_x = scroll_speed * frame_dt * 1.5; // characters are taller than they are wide double scroll_amount_y = scroll_speed * frame_dt; if (ted_is_key_down(ted, SDLK_UP)) buffer_scroll(active_buffer, 0, -scroll_amount_y); if (ted_is_key_down(ted, SDLK_DOWN)) buffer_scroll(active_buffer, 0, +scroll_amount_y); if (ted_is_key_down(ted, SDLK_LEFT)) buffer_scroll(active_buffer, -scroll_amount_x, 0); if (ted_is_key_down(ted, SDLK_RIGHT)) buffer_scroll(active_buffer, +scroll_amount_x, 0); } if (menu_is_any_open(ted)) { menu_update(ted); } for (int i = 0; ted->lsps[i]; ++i) { LSP *lsp = ted->lsps[i]; LSPMessage message = {0}; while (lsp_next_message(lsp, &message)) { switch (message.type) { case LSP_REQUEST: { LSPRequest *r = &message.request; switch (r->type) { case LSP_REQUEST_SHOW_MESSAGE: { LSPRequestMessage *m = &r->data.message; MessageType type = ted_message_type_from_lsp(m->type); ted_set_message(ted, type, "%s", lsp_request_string(r, m->message)); } break; case LSP_REQUEST_LOG_MESSAGE: { LSPRequestMessage *m = &r->data.message; ted_log(ted, "%s\n", lsp_request_string(r, m->message)); } break; case LSP_REQUEST_PUBLISH_DIAGNOSTICS: { ted_process_publish_diagnostics(ted, lsp, r); } break; default: break; } } break; case LSP_RESPONSE: { LSPResponse *r = &message.response; if (!lsp_string_is_empty(r->error)) { ted_log(ted, "LSP error: %s\n", lsp_response_string(r, r->error)); // this is a bit spammy // sometimes clang is just like "this request was cancelled cuz the cursor moved" //ted_error(ted, "LSP error: %s", lsp_response_string(r, r->error)); // it's important that we send error responses here too. // we don't want to be waiting around for a response that's never coming. autocomplete_process_lsp_response(ted, r); format_process_lsp_response(ted, r); signature_help_process_lsp_response(ted, r); hover_process_lsp_response(ted, r); definitions_process_lsp_response(ted, lsp, r); highlights_process_lsp_response(ted, r); usages_process_lsp_response(ted, r); document_link_process_lsp_response(ted, r); rename_symbol_process_lsp_response(ted, r); } } break; } lsp_message_free(&message); } } ted_update_window_dimensions(ted); float window_width = ted->window_width, window_height = ted->window_height; // set up GL glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glViewport(0, 0, (GLsizei)window_width, (GLsizei)window_height); { // clear (background) float bg_color[4]; color_u32_to_floats(ted_active_color(ted, COLOR_BG), bg_color); glClearColor(bg_color[0], bg_color[1], bg_color[2], bg_color[3]); } glClear(GL_COLOR_BUFFER_BIT); { // background shader const Settings *s = ted_active_settings(ted); if (s->bg_shader) { GLuint shader = s->bg_shader->shader; GLuint buffer = s->bg_shader->buffer; GLuint array = s->bg_shader->array; glUseProgram(shader); if (array) glBindVertexArray(array); double t = ted->frame_time; glUniform1f(glGetUniformLocation(shader, "t_time"), (float)fmod(t - start_time, 3600)); glUniform2f(glGetUniformLocation(shader, "t_aspect"), (float)window_width / (float)window_height, 1); glUniform1f(glGetUniformLocation(shader, "t_save_time"), (float)(t - ted->last_save_time)); if (s->bg_texture) { GLuint texture = s->bg_texture->texture; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glUniform1i(glGetUniformLocation(shader, "t_texture"), 0); } else { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); } glBindBuffer(GL_ARRAY_BUFFER, buffer); if (!array) { GLuint v_pos = (GLuint)glGetAttribLocation(shader, "v_pos"); glVertexAttribPointer(v_pos, 2, GL_FLOAT, 0, 2 * sizeof(float), 0); glEnableVertexAttribArray(v_pos); } glDrawArrays(GL_TRIANGLES, 0, 6); } } Font *font = ted->font; // default window title ted_set_window_title(ted, "ted"); { const float padding = ted_active_settings(ted)->padding; float x1 = padding, y = window_height-padding, x2 = window_width-padding; if (ted->find) { float y2 = y; y -= find_menu_height(ted); find_menu_frame(ted, rect4(x1, y, x2, y2)); y -= padding; } if (ted->build_shown) { float y2 = y; y -= ted->build_output_height * ted->window_height; if (ted->build_output_height == 0) { // hasn't been initialized yet ted->build_output_height = 0.25f; } if (ted->resizing_build_output) { if (ted->mouse_state & SDL_BUTTON_LMASK) { // resize it ted->build_output_height = clampf((y2 - ted_mouse_pos(ted).y) / ted->window_height, 0.05f, 0.8f); } else { // stop resizing build output ted->resizing_build_output = false; } ted->cursor = ted->cursor_resize_v; } else { Rect gap = rect4(x1, y - padding, x2, y); if (ted_clicked_in_rect(ted, gap)) { // start resizing build output ted->resizing_build_output = true; } if (ted_mouse_in_rect(ted, gap)) { ted->cursor = ted->cursor_resize_v; } } build_frame(ted, x1, y, x2, y2); y -= padding; } if (arr_len(ted->nodes)) { Node *node = ted->nodes[0]; float y1 = padding; node_frame(ted, node, rect4(x1, y1, x2, y)); autocomplete_frame(ted); signature_help_frame(ted); hover_frame(ted, frame_dt); definitions_frame(ted); highlights_frame(ted); usages_frame(ted); document_link_frame(ted); rename_symbol_frame(ted); } else { autocomplete_close(ted); if (!ted->build_shown) { text_utf8_anchored(font, "ted v. " TED_VERSION, window_width - padding, window_height - padding, ted_active_color(ted, COLOR_COMMENT), ANCHOR_BOTTOM_RIGHT); text_utf8_anchored(font, "Press Ctrl+O to open a file or Ctrl+N to create a new one.", window_width * 0.5f, window_height * 0.5f, ted_active_color(ted, COLOR_COMMENT), ANCHOR_MIDDLE); text_render(font); } } } // stop dragging tab if mouse was released if (arr_len(ted->mouse_releases[SDL_BUTTON_LEFT])) ted->dragging_tab_node = NULL; if (menu_is_any_open(ted)) { menu_render(ted); } if (text_has_err()) { ted_error(ted, "Couldn't render text: %s", text_get_err()); } arr_foreach_ptr(ted->buffers, TextBufferPtr, pbuffer) { TextBuffer *buffer = *pbuffer; if (buffer_has_error(buffer)) { ted_error_from_buffer(ted, buffer); buffer_clear_error(buffer); } } for (int i = 0; ted->lsps[i]; ++i) { LSP *lsp = ted->lsps[i]; char error[512] = {0}; if (lsp_get_error(lsp, error, sizeof error, true)) { ted_error(ted, "%s", error); } } // check if there's a new error if (*ted->message) { ted->message_time = ted->frame_time; str_cpy(ted->message_shown, sizeof ted->message_shown, ted->message); ted->message_shown_type = ted->message_type; *ted->message = '\0'; } // message box if (*ted->message_shown) { double time_passed = ted->frame_time - ted->message_time; const Settings *settings = ted_active_settings(ted); if (time_passed > settings->error_display_time) { // stop showing error *ted->message_shown = '\0'; } else { Rect r = message_box_rect(ted); float padding = settings->padding; ColorSetting bg_color=0, border_color=0; ted_color_settings_for_message_type(ted->message_type, &bg_color, &border_color); gl_geometry_rect(r, ted_active_color(ted, bg_color)); gl_geometry_rect_border(r, settings->border_thickness, ted_active_color(ted, border_color)); float text_x1 = rect_x1(r) + padding, text_x2 = rect_x2(r) - padding; float text_y1 = rect_y1(r) + padding; // (make sure text wraps) TextRenderState text_state = text_render_state_default; text_state.min_x = text_x1; text_state.max_x = text_x2; text_state.x = text_x1; text_state.y = text_y1; text_state.wrap = true; color_u32_to_floats(ted_active_color(ted, COLOR_TEXT), text_state.color); text_utf8_with_state(font, &text_state, ted->message_shown); gl_geometry_draw(); text_render(font); } } if (ted->recording_macro) { Font *font_bold = ted->font_bold; u32 bg_color = ted_active_color(ted, COLOR_ERROR_BG); u32 color = ted_active_color(ted, COLOR_TEXT); u8 padding = ted_active_settings(ted)->padding; const char *text = "Recording macro..."; vec2 size = text_get_size_vec2(font_bold, text); Rect r = { .pos = vec2_sub((vec2){window_width - 3 * padding, window_height - 3 * padding}, size), .size = vec2_add(size, (vec2){padding, padding}), }; gl_geometry_rect(r, bg_color); Rect full_screen = { .pos = {0, 0}, .size = {window_width, window_height} }; gl_geometry_rect(full_screen, bg_color & 0xffffff0f); text_utf8_anchored(font_bold, text, window_width - 2.5f * padding, window_height - 2.5f * padding, color, ANCHOR_BOTTOM_RIGHT); gl_geometry_draw(); text_render(font_bold); } ted_check_for_node_problems(ted); #if !NDEBUG for (u16 i = 0; i < arr_len(ted->buffers); ++i) buffer_check_valid(ted->buffers[i]); buffer_check_valid(ted->line_buffer); buffer_check_valid(ted->argument_buffer); buffer_check_valid(ted->find_buffer); buffer_check_valid(ted->replace_buffer); buffer_check_valid(ted->build_buffer); #endif if (ted->dragging_tab_node) ted->cursor = ted->cursor_move; SDL_SetWindowTitle(window, ted->window_title); if (ted->cursor) { SDL_SetCursor(ted->cursor); SDL_ShowCursor(SDL_ENABLE); } else { SDL_ShowCursor(SDL_DISABLE); } double frame_end_noswap = time_get_seconds(); #if !PROFILE_FRAME { // annoyingly, SDL_GL_SwapWindow seems to be a busy loop on my laptop for some reason... // this is why the framerate-cap settings exists const Settings *settings = ted_default_settings(ted); if (settings->framerate_cap) { i32 ms_wait = 1000 / (i32)settings->framerate_cap - (i32)((frame_end_noswap - frame_start) * 1000); ms_wait -= 1; // give swap an extra ms to make sure it's actually vsynced if (ms_wait > 0) { SDL_Delay((u32)ms_wait); } } // i don't know if SDL_GL_SetSwapInterval is slow on any platform // (if you're not actually changing it). just in case, let's make sure // we only call it when the vsync setting actually changes. static int prev_vsync = -1; if (settings->vsync != prev_vsync) { prev_vsync = settings->vsync; SDL_GL_SetSwapInterval(settings->vsync ? 1 : 0); } } #else (void)frame_end_noswap; SDL_GL_SetSwapInterval(0); #endif SDL_GL_SwapWindow(window); PROFILE_TIME(frame_end) assert(glGetError() == 0); #if PROFILE_FRAME { print("Frame: %.1f ms\n", (frame_end - frame_start) * 1000); } #endif if (test) { ted_test(ted); break; } } for (size_t i = 0; i < arr_count(ted->mouse_clicks); ++i) arr_clear(ted->mouse_clicks[i]); for (size_t i = 0; i < arr_count(ted->mouse_releases); ++i) arr_clear(ted->mouse_releases[i]); if (ted->find) find_close(ted); build_stop(ted); if (menu_is_any_open(ted)) menu_close(ted); hover_quit(ted); signature_help_quit(ted); autocomplete_quit(ted); format_quit(ted); highlights_quit(ted); usages_quit(ted); session_write(ted); rename_symbol_quit(ted); document_link_quit(ted); definitions_quit(ted); menu_quit(ted); arr_free(ted->edit_notifys); file_selector_free(ted->file_selector); ted->file_selector = NULL; for (int i = 0; i < TED_LSP_MAX; ++i) { if (!ted->lsps[i]) break; lsp_free(ted->lsps[i]); ted->lsps[i] = NULL; } lsp_quit(); syntax_quit(); arr_foreach_ptr(ted->shell_history, char *, cmd) { free(*cmd); } arr_free(ted->shell_history); fclose(ted->log), ted->log = NULL; SDL_FreeCursor(ted->cursor_arrow); SDL_FreeCursor(ted->cursor_ibeam); SDL_FreeCursor(ted->cursor_wait); SDL_FreeCursor(ted->cursor_resize_h); SDL_FreeCursor(ted->cursor_resize_v); SDL_FreeCursor(ted->cursor_hand); SDL_FreeCursor(ted->cursor_move); SDL_GL_DeleteContext(glctx); SDL_DestroyWindow(window); SDL_Quit(); for (u16 i = 0; i < arr_len(ted->buffers); ++i) buffer_free(ted->buffers[i]); arr_clear(ted->buffers); arr_foreach_ptr(ted->nodes, NodePtr, pnode) { node_free(*pnode); } arr_clear(ted->nodes); buffer_free(ted->line_buffer); buffer_free(ted->find_buffer); buffer_free(ted->replace_buffer); buffer_free(ted->build_buffer); buffer_free(ted->argument_buffer); ted_free_fonts(ted); config_free_all(ted); macros_free(ted); free(ted); #if _WIN32 for (int i = 0; i < argc; ++i) free(argv[i]); free(argv); #endif return 0; }