A backup of edit's original C code

This commit is contained in:
Leonard Hecker 2025-05-21 14:09:39 +02:00
commit 27b2e77f7a
29 changed files with 7680 additions and 0 deletions

21
.clang-format Normal file
View File

@ -0,0 +1,21 @@
---
Language: Cpp
BasedOnStyle: Microsoft
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortFunctionsOnASingleLine: All
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Linux
ColumnLimit: 0
IncludeCategories:
- Regex: '^<.*'
- Regex: '^".*'
InsertNewlineAtEOF: true
LineEnding: LF
MaxEmptyLinesToKeep: 1
NamespaceIndentation: All
PointerAlignment: Left
SeparateDefinitionBlocks: Always
SpaceAfterTemplateKeyword: false

2
.clang-tidy Normal file
View File

@ -0,0 +1,2 @@
---
Checks: 'clang-diagnostic-*,clang-analyzer-*'

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea
.vs
*.user
bin
CMakeSettings.json
obj
out
target

122
CMakeLists.txt Normal file
View File

@ -0,0 +1,122 @@
cmake_minimum_required(VERSION 3.25)
# Enable CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
cmake_policy(SET CMP0141 NEW)
project(tui LANGUAGES C)
add_executable(
tui
src/arena.c
src/buffer.c
src/helpers.c
src/icu.c
src/input.c
src/loc.c
src/main.c
src/os_win32.c
src/tui.c
src/tui.exe.manifest
src/ucd.c
src/vt.c
)
set_target_properties(
tui
PROPERTIES
C_STANDARD 23
CXX_STANDARD 20
)
if (MSVC)
# Enable hot reload for Debug builds.
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<CONFIG:Debug>,EditAndContinue,ProgramDatabase>")
# We want to always use the DLL runtime, even for Release builds.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
# MSVC currently doesn't have a switch for C23 and CMake only supports C17 for MSVC.
set(CMAKE_C23_STANDARD_COMPILE_OPTION "/std:clatest")
foreach (lang C CXX)
# CMake by default manually specifies a list of libs for linking.
# Why? Just use the default imports.
#set("CMAKE_${lang}_STANDARD_LIBRARIES" "")
string(
CONCAT
"CMAKE_${lang}_FLAGS"
# Allow the source code to identify the language version.
"/Zc:__STDC__ /Zc:__cplusplus "
# Eww, who needs RTTI?
"/GR- "
# Avoids putting inline functions into .obj files.
# Reduces their size sometimes significantly.
"/Zc:inline "
# > For a new project, it may be best to use /W4 in all compilations.
# > This option helps ensure the fewest possible hard-to-find code defects.
"/W4 /wd4100 "
# MSVC is still living in the 80s. Tell it to wake up.
"/utf-8 "
# Deterministic compilation? Great!
"/experimental:deterministic "
# Enables new, neat console output diagnostics.
"/diagnostics:caret"
)
string(
CONCAT
"CMAKE_${lang}_FLAGS_DEBUG"
# Produce a PDB file in a format that supports the Edit and Continue feature.
"/Zi "
)
foreach (mode RELEASE RELWITHDEBINFO)
string(
CONCAT
"CMAKE_${lang}_FLAGS_${mode}"
# Turn optimizations on.
"/O2 "
# Produce a separate PDB file.
"/Zi "
# NDEBUG is a standard C/C++ precompiler flag for Release mode.
"/DNDEBUG "
# This will go hand in hand with the linker instructions below.
# Currently disabled until I decide if I want to nuke the CRT imports or not.
#"/DNODEFAULTLIB"
)
endforeach ()
endforeach ()
foreach (mode RELEASE RELWITHDEBINFO)
string(
CONCAT
"CMAKE_EXE_LINKER_FLAGS_${mode}"
# Ensure we get a nice monolithic PDB.
"/DEBUG:FULL "
# /INCREMENTAL:NO and /LTCG are important for small binaries.
"/INCREMENTAL:NO /LTCG "
# /DEBUG disables /OPT:REF and /OPT:ICF which is bad for the binary size.
"/OPT:REF,ICF "
# This ensures our binary gets a checksum.
"/RELEASE "
# Remove the default CRT, vcruntime, etc. imports.
# We want tight control over what gets imported so that it is tiny and runs everywhere.
"/NODEFAULTLIB "
# ucrt provides the C runtime functions (printf, etc.)
"ucrt.lib "
# Confusingly, vcruntime provides memcpy and memset. God knows why it's not in ucrt.
"vcruntime.lib "
# msvcrt provides the /GS (Buffer Security Check) support code and __isa_available_init.
"msvcrt.lib "
# Provides the exports for the subset of Win32 APIs
# that are common to all Windows 10 devices, and later.
"OneCore.lib"
)
endforeach ()
elseif (WIN32)
# The clang-cl distribution from Visual Studio currently doesn't support /Zc:__STDC__.
# I couldn't find a different workaround.
target_compile_definitions(tui PRIVATE __STDC__)
target_compile_options(tui PRIVATE -Wall -Werror -Wno-unused-function)
endif ()

44
CMakePresets.json Normal file
View File

@ -0,0 +1,44 @@
{
"version": 3,
"configurePresets": [
{
"name": "windows-base",
"description": "Target Windows with the Visual Studio development environment.",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"installDir": "${sourceDir}/out/install/${presetName}",
"cacheVariables": {
"CMAKE_C_COMPILER": "cl.exe",
"CMAKE_CXX_COMPILER": "cl.exe"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "x64-debug",
"displayName": "x64 Debug",
"description": "Target Windows (64-bit) with the Visual Studio development environment. (Debug)",
"inherits": "windows-base",
"architecture": {
"value": "x64",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "x64-release",
"displayName": "x64 Release",
"description": "Target Windows (64-bit) with the Visual Studio development environment. (RelWithDebInfo)",
"inherits": "x64-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

264
src/arena.c Normal file
View File

@ -0,0 +1,264 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "arena.h"
#include <stdlib.h>
#include <threads.h>
#include "os.h"
#ifdef _MSC_VER
#include <intrin.h>
#endif
#define ALLOC_CHUNK_SIZE (usize)(64 * 1024)
Arena* arena_create(usize capacity)
{
capacity = (capacity + ALLOC_CHUNK_SIZE - 1) & ~(ALLOC_CHUNK_SIZE - 1);
void* base = os_virtual_reserve(capacity);
if (!base) {
abort();
}
Arena temp = {
.capacity = capacity,
.base = base,
};
Arena* arena = arena_malloc(&temp, Arena);
memcpy(arena, &temp, sizeof(Arena));
return arena;
}
void arena_reset(Arena* arena)
{
#ifndef NDEBUG
memset(arena->base + sizeof(Arena), 0xDD, arena->offset);
#endif
arena->offset = sizeof(Arena);
}
void arena_destroy(Arena* arena)
{
os_virtual_release(arena->base);
}
static thread_local Arena* s_scratch[2];
attribute_noinline static void scratch_init()
{
for (int i = 0; i < 2; i++) {
s_scratch[i] = arena_create(64 * 1024 * 1024);
}
}
ScratchArena scratch_beg(Arena* conflict)
{
if (!s_scratch[0]) {
scratch_init();
}
Arena* arena = s_scratch[conflict == s_scratch[0]];
return (ScratchArena){arena, arena->offset};
}
void scratch_end(ScratchArena scratch)
{
#ifndef NDEBUG
memset(scratch.arena->base + scratch.offset, 0xDD, scratch.arena->offset - scratch.offset);
#endif
scratch.arena->offset = scratch.offset;
}
void* arena_malloc_raw(Arena* arena, usize bytes, usize alignment)
{
bytes = max(bytes, 1);
alignment = max(alignment, 1);
usize offset_old = arena->offset;
usize beg = (offset_old + alignment - 1) & ~(alignment - 1);
usize end = beg + bytes;
u8* ptr = arena->base + beg;
arena->offset = end;
if (end > arena->commit) {
usize commit_old = arena->commit;
usize commit_new = (end + ALLOC_CHUNK_SIZE - 1) & ~(ALLOC_CHUNK_SIZE - 1);
if (commit_new > arena->capacity) {
abort();
}
os_virtual_commit(arena->base + commit_old, commit_new - commit_old);
arena->commit = commit_new;
}
#ifndef NDEBUG
end += 128;
end = min(end, arena->commit);
memset(arena->base + offset_old, 0xCD, end - offset_old);
#endif
return ptr;
}
void* arena_zalloc_raw(Arena* arena, usize bytes, usize alignment)
{
void* ptr = arena_malloc_raw(arena, bytes, alignment);
memset(ptr, 0, bytes);
return ptr;
}
void slice_reserve(Arena* arena, Slice* dst, usize cap, usize size, usize alignment)
{
if (cap <= dst->cap) {
return;
}
alignment = max(alignment, 1);
u8* beg_old = dst->beg;
usize cap_old = dst->cap;
usize cap_new = dst->cap * 2;
cap_new = max(cap_new, cap);
cap_new = max(cap_new, 128);
usize bytes_old = cap_old * size;
usize bytes_new = cap_new * size;
dst->cap = cap_new;
if (beg_old + bytes_old == arena->base + arena->offset) {
arena_malloc_raw(arena, bytes_new - bytes_old, 1);
} else {
dst->beg = arena_malloc_raw(arena, bytes_new, alignment);
memcpy(dst->beg, beg_old, bytes_old);
}
}
void s8_append(Arena* arena, s8* dst, s8 suffix)
{
if (suffix.len == 0) {
return;
}
slice_reserve(arena, (Slice*)dst, dst->len + suffix.len, 1, 1);
memcpy(dst->beg + dst->len, suffix.beg, suffix.len);
dst->len += suffix.len;
}
void s8_append_repeat(Arena* arena, s8* dst, c8 ch, usize count)
{
if (count == 0) {
return;
}
slice_reserve(arena, (Slice*)dst, dst->len + count, 1, 1);
memset(dst->beg + dst->len, ch, count);
dst->len += count;
}
void s8_append_repeat_string(Arena* arena, s8* dst, s8 rep, usize count)
{
if (count == 0) {
return;
}
usize total_len = rep.len * count;
slice_reserve(arena, (Slice*)dst, dst->len + total_len, 1, 1);
u8* dst_beg = dst->beg + dst->len;
u8* dst_it = dst_beg;
u8* src = rep.beg;
usize remaining = total_len;
usize len_next = 0;
usize len = rep.len;
dst->len += total_len;
// This loop efficiently repeats the string `rep` `count` times by doubling its length in each iteration.
// I.e. "abc" -> "abcabc" -> "abcabcabcabc" -> "abcabcabcabcabcabcabcabc" -> ...
// It uses `len` / `len_next` to ensure that the 1st and 2nd iteration both operate with `len == rep.len`,
// since in the 1st iteration we need to copy `rep` into `dst` and in the 2nd iteration we perform the first doubling.
for (;;) {
memcpy(dst_it, src, len);
remaining -= len;
if (remaining == 0) {
return;
}
dst_it += len;
src = dst_beg;
len_next += len;
len_next = min(len_next, remaining);
len = len_next;
}
}
static void write_decimal(Arena* arena, s8* dst, u64 v, bool neg)
{
// Mapping 2 digits at a time speeds things up a lot because half the divisions are necessary.
// I got this idea from https://github.com/fmtlib/fmt which in turn got it
// from the talk "Three Optimization Tips for C++" by Andrei Alexandrescu.
static const u8 lut[] =
"0001020304050607080910111213141516171819"
"2021222324252627282930313233343536373839"
"4041424344454647484950515253545556575859"
"6061626364656667686970717273747576777879"
"8081828384858687888990919293949596979899";
const u64 log10 = u64_log10(v);
u64 digits = log10 + 1;
usize new_len = dst->len + digits + neg;
slice_reserve(arena, (Slice*)dst, new_len, 1, 1);
dst->len = new_len;
u8* end = dst->beg + new_len;
u8* p = end;
while (digits > 1) {
const u8* s = &lut[(v % 100) * 2];
*--p = s[1];
*--p = s[0];
v /= 100;
digits -= 2;
}
if (digits & 1) {
*--p = (u8)('0' + v);
}
if (neg) {
*p = '-';
}
}
void s8_append_i64(Arena* arena, s8* dst, i64 value)
{
write_decimal(arena, dst, value < 0 ? -value : value, value < 0);
}
void s8_append_u64(Arena* arena, s8* dst, u64 value)
{
write_decimal(arena, dst, value, false);
}
void s8_replace(Arena* arena, s8* dst, usize beg, usize end, s8 replacement)
{
usize len_old = dst->len;
end = min(end, len_old);
beg = min(beg, end);
usize len_new = dst->len - (end - beg) + replacement.len;
slice_reserve(arena, (Slice*)dst, len_new, 1, 1);
memmove(dst->beg + beg + replacement.len, dst->beg + end, len_old - end);
memcpy(dst->beg + beg, replacement.beg, replacement.len);
dst->len = len_new;
}

155
src/arena.h Normal file
View File

@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "helpers.h"
typedef struct Arena {
u8* base;
usize capacity;
usize commit;
usize offset;
} Arena;
Arena* arena_create(usize capacity);
void arena_reset(Arena* arena);
void arena_destroy(Arena* arena);
typedef struct ScratchArena {
Arena* arena;
usize offset;
} ScratchArena;
ScratchArena scratch_beg(Arena* conflict);
void scratch_end(ScratchArena scratch);
void* arena_malloc_raw(Arena* arena, usize bytes, usize alignment);
void* arena_zalloc_raw(Arena* arena, usize bytes, usize alignment);
#define arena_malloc(arena, tp) (tp*)arena_malloc_raw(arena, sizeof(tp), max(_Alignof(tp), 16))
#define arena_zalloc(arena, tp) (tp*)arena_zalloc_raw(arena, sizeof(tp), max(_Alignof(tp), 16))
#define arena_mallocn(arena, tp, n) (tp*)arena_malloc_raw(arena, n * sizeof(tp), max(_Alignof(tp), 16))
#define arena_zallocn(arena, tp, n) (tp*)arena_zalloc_raw(arena, n * sizeof(tp), max(_Alignof(tp), 16))
typedef struct Slice {
void* beg;
usize len;
usize cap;
} Slice;
void slice_reserve(Arena* arena, Slice* dst, usize cap, usize size, usize alignment);
#define slice_push(arena, dst, ...) \
do { \
__typeof__(__VA_ARGS__) item = __VA_ARGS__; \
if ((dst)->len >= (dst)->cap) { \
slice_reserve(arena, (Slice*)(dst), (dst)->len + 1, _Alignof(__typeof__(item)), sizeof(item)); \
} \
(dst)->beg[(dst)->len++] = item; \
} while (0)
#define s8_reserve(arena, dst, cap) slice_reserve(arena, (Slice*)dst, cap, 1, 1)
void s8_append(Arena* arena, s8* dst, s8 suffix);
void s8_append_repeat(Arena* arena, s8* dst, c8 ch, usize count);
void s8_append_repeat_string(Arena* arena, s8* dst, s8 rep, usize count);
void s8_append_i64(Arena* arena, s8* dst, i64 value);
void s8_append_u64(Arena* arena, s8* dst, u64 value);
void s8_replace(Arena* arena, s8* dst, usize beg, usize end, s8 replacement);
attribute_forceinline void s8_append_literal(Arena* arena, s8* dst, const char* str)
{
#pragma warning(suppress : 4210)
extern size_t strlen(char const*);
usize len = strlen(str);
s8_append(arena, dst, (s8){(c8*)str, len, len});
}
// clang-format off
#define s8_append_auto(arena, dst, value) _Generic((value), \
char*: s8_append_literal, \
s8: s8_append, \
signed char: s8_append_i64, \
signed short: s8_append_i64, \
signed int: s8_append_i64, \
signed long: s8_append_i64, \
signed long long: s8_append_i64, \
unsigned char: s8_append_u64, \
unsigned short: s8_append_u64, \
unsigned int: s8_append_u64, \
unsigned long: s8_append_u64, \
unsigned long long: s8_append_u64 \
)(arena, dst, value)
// clang-format on
#define s8_append_fmt(arena, dst, ...) \
do { \
__VA_ARGS_HELPER(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) \
(arena, dst, __VA_ARGS__); \
} while (0)
#define __VA_ARGS_HELPER(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) __VA_ARGS_HELPER_##N
#define __VA_ARGS_HELPER_1(arena, dst, a1) \
s8_append_auto(arena, dst, a1)
#define __VA_ARGS_HELPER_2(arena, dst, a1, a2) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2)
#define __VA_ARGS_HELPER_3(arena, dst, a1, a2, a3) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3)
#define __VA_ARGS_HELPER_4(arena, dst, a1, a2, a3, a4) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4)
#define __VA_ARGS_HELPER_5(arena, dst, a1, a2, a3, a4, a5) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5)
#define __VA_ARGS_HELPER_6(arena, dst, a1, a2, a3, a4, a5, a6) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5); \
s8_append_auto(arena, dst, a6)
#define __VA_ARGS_HELPER_7(arena, dst, a1, a2, a3, a4, a5, a6, a7) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5); \
s8_append_auto(arena, dst, a6); \
s8_append_auto(arena, dst, a7)
#define __VA_ARGS_HELPER_8(arena, dst, a1, a2, a3, a4, a5, a6, a7, a8) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5); \
s8_append_auto(arena, dst, a6); \
s8_append_auto(arena, dst, a7); \
s8_append_auto(arena, dst, a8)
#define __VA_ARGS_HELPER_9(arena, dst, a1, a2, a3, a4, a5, a6, a7, a8, a9) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5); \
s8_append_auto(arena, dst, a6); \
s8_append_auto(arena, dst, a7); \
s8_append_auto(arena, dst, a8); \
s8_append_auto(arena, dst, a9)
#define __VA_ARGS_HELPER_10(arena, dst, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) \
s8_append_auto(arena, dst, a1); \
s8_append_auto(arena, dst, a2); \
s8_append_auto(arena, dst, a3); \
s8_append_auto(arena, dst, a4); \
s8_append_auto(arena, dst, a5); \
s8_append_auto(arena, dst, a6); \
s8_append_auto(arena, dst, a7); \
s8_append_auto(arena, dst, a8); \
s8_append_auto(arena, dst, a9); \
s8_append_auto(arena, dst, a10)

782
src/buffer.c Normal file
View File

@ -0,0 +1,782 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "buffer.h"
#include "os.h"
#include "ucd.h"
#include <stdlib.h>
#define TEXT_BUFFER_ALLOC_MAX_BYTES 0x80000000 // 2GiB
#define TEXT_BUFFER_ALLOC_CHUNK_BYTES 0x10000 // 64KiB
#define TEXT_BUFFER_GAP_CHUNK_BYTES 0x1000 // 4KiB
static void* text_buffer_allocator_default_realloc(void* base, usize old_len, usize* new_len)
{
if (!base) {
base = os_virtual_reserve(TEXT_BUFFER_ALLOC_MAX_BYTES);
}
usize len = *new_len;
len = (len + TEXT_BUFFER_ALLOC_CHUNK_BYTES - 1) & ~(TEXT_BUFFER_ALLOC_CHUNK_BYTES - 1);
if (len > TEXT_BUFFER_ALLOC_MAX_BYTES) {
abort();
}
os_virtual_commit((u8*)base + old_len, len - old_len);
*new_len = len;
return base;
}
static void text_buffer_allocator_default_free(void* base, usize len)
{
if (base) {
os_virtual_release(base);
}
}
TextBufferAllocator text_buffer_allocator_default()
{
return (TextBufferAllocator){
.realloc = text_buffer_allocator_default_realloc,
.free = text_buffer_allocator_default_free,
};
}
TextBuffer* text_buffer_create(TextBufferAllocator allocator)
{
assert(allocator.realloc);
assert(allocator.free);
usize commit = sizeof(TextBuffer);
TextBuffer* tb = allocator.realloc(NULL, 0, &commit);
if (!tb) {
abort();
}
*tb = (TextBuffer){
.allocator = allocator,
.main_base = (c8*)tb,
.main_commit = commit,
.text = (c8*)(tb + 1),
.word_wrap_columns = -1,
};
return tb;
}
void text_buffer_destroy(TextBuffer* tb)
{
tb->allocator.free(tb->undo_base, tb->undo_commit);
tb->allocator.free(tb->main_base, tb->main_commit);
}
static void text_buffer_allocate_gap_impl(TextBuffer* tb, usize off, usize len)
{
c8* data = tb->text;
const usize length = tb->text_length;
const usize gap_off = tb->gap_off;
const usize gap_len = tb->gap_len;
off = min(off, length);
// Move the existing gap if it exists
if (off != gap_off) {
if (gap_len > 0) {
//
// v gap_off
// left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
// |ABCDEFGHI JKLMNOPQRSTUVWXYZ|
// ^ off
// move: JKLMN
//
// v gap_off
// !left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
// |ABCDEFGHIJKLMNOPQRS TUVWXYZ|
// ^ off
// move: OPQRS
//
const bool left = off < gap_off;
const usize move_src = left ? off : gap_off + gap_len;
const usize move_dst = left ? off + gap_len : gap_off;
const usize move_len = left ? gap_off - off : off - gap_off;
memmove(data + move_dst, data + move_src, move_len);
#ifndef NDEBUG
memset(data + off, 0xCD, gap_len);
#endif
}
tb->gap_off = off;
}
// Enlarge the gap if needed
if (len > gap_len) {
usize gap_chunk_pad_chars = TEXT_BUFFER_GAP_CHUNK_BYTES / 2;
usize gap_len_new = (len + gap_chunk_pad_chars + TEXT_BUFFER_GAP_CHUNK_BYTES - 1) & ~(TEXT_BUFFER_GAP_CHUNK_BYTES - 1);
usize bytes_old = tb->main_commit;
// The TextBuffer struct is stored in front of the .text data. As such, we need to account for it.
usize bytes_new = sizeof(TextBuffer) + length + gap_len_new;
if (bytes_new > bytes_old) {
tb->main_base = tb->allocator.realloc(tb->main_base, bytes_old, &bytes_new);
tb->main_commit = bytes_new;
}
c8* gap_beg = data + off;
memmove(gap_beg + gap_len_new, gap_beg + gap_len, length - off);
#ifndef NDEBUG
memset(gap_beg + gap_len, 0xCD, gap_len_new - gap_len);
#endif
tb->gap_len = gap_len_new;
}
}
static c8* text_buffer_allocate_gap(TextBuffer* tb, const usize off, const usize len)
{
if (off != tb->gap_off || len > tb->gap_len) {
text_buffer_allocate_gap_impl(tb, off, len);
}
return tb->text + off;
}
static void text_buffer_commit_gap(TextBuffer* tb, usize len)
{
len = min(len, tb->gap_len);
tb->gap_off += len;
tb->gap_len -= len;
tb->text_length += len;
}
s8 text_buffer_read_backward(const TextBuffer* tb, usize off)
{
usize beg;
usize len;
if (off <= tb->gap_off) {
// Cursor is before the gap: We can read until the beginning of the buffer.
beg = 0;
len = off;
} else {
// Cursor is after the gap: We can read until the end of the gap.
beg = tb->gap_off + tb->gap_len;
// The cursor_off doesn't account of the gap_len.
// (This allows us to move the gap without recalculating the cursor position.)
len = off - tb->gap_off;
}
return (s8){tb->text + beg, len, len};
}
s8 text_buffer_read_forward(const TextBuffer* tb, usize off)
{
usize beg;
usize len;
if (off < tb->gap_off) {
// Cursor is before the gap: We can read until the start of the gap.
beg = off;
len = tb->gap_off - off;
} else {
// Cursor is after the gap: We can read until the end of the buffer.
beg = off + tb->gap_len;
len = tb->text_length - off;
}
return (s8){tb->text + beg, len, len};
}
static void text_buffer_record_push_change(TextBuffer* tb, usize beg, usize end, s8 replacement)
{
if (beg > end || end > tb->text_length) {
assert(false);
return;
}
usize removed_len = end - beg;
usize space_needed = sizeof(TextBufferChange) + removed_len + replacement.len;
usize usage_now = tb->undo_usage;
usize usage_next = usage_now + space_needed;
if (usage_next > tb->undo_commit) {
tb->undo_base = tb->allocator.realloc(tb->undo_base, tb->undo_commit, &usage_next);
tb->undo_commit = usage_next;
if (tb->undo_tail) {
// This ensures that text_buffer_redo() can differentiate between
// an empty undo stack and one that has simply been fully undone.
memset(tb->undo_base, 0, sizeof(TextBufferChange));
}
}
TextBufferChange* change = (TextBufferChange*)((u8*)tb->undo_base + tb->undo_usage);
s8 removed = {(c8*)(change + 1), removed_len, removed_len};
s8 inserted = {removed.beg + removed.len, replacement.len, replacement.len};
removed.len = text_buffer_extract(tb, beg, end, removed.beg);
memcpy(inserted.beg, replacement.beg, replacement.len);
if (tb->undo_tail) {
tb->undo_tail->next = change;
}
change->prev = tb->undo_tail;
change->next = NULL;
change->cursor = tb->cursor;
change->removed = removed;
change->inserted = inserted;
tb->undo_tail = change;
tb->undo_usage = usage_next;
tb->dirty = true;
}
enum CodePage_t {
CP_UTF7 = 65000,
CP_UTF8 = 65001,
CP_UTF16LE = 1200,
CP_UTF16BE = 1201,
CP_UTF32LE = 12000,
CP_UTF32BE = 12001,
CP_GB18030 = 54936,
};
static int detect_bom(const c8* buffer, int read, u32* cp_out)
{
u32 cp = 0;
int len = 0;
if (read >= 4) {
len = 4;
if (memcmp(&buffer[0], "\xFF\xFE\x00\x00", 4) == 0) {
cp = CP_UTF32LE;
} else if (memcmp(&buffer[0], "\x00\x00\xFE\xFF", 4) == 0) {
cp = CP_UTF32BE;
} else if (memcmp(&buffer[0], "\x84\x31\x95\x33", 4) == 0) {
cp = CP_GB18030;
} else {
len = 0;
}
}
if (cp == 0 && read >= 3) {
len = 3;
if (memcmp(&buffer[0], "\xEF\xBB\xBF", 3) == 0) {
cp = CP_UTF8;
}
if (memcmp(&buffer[0], "\x2B\x2F\x76", 3) == 0) {
cp = CP_UTF7;
} else {
len = 0;
}
}
if (cp == 0 && read >= 2) {
len = 2;
if (memcmp(&buffer[0], "\xFF\xFE", 2) == 0) {
cp = CP_UTF16LE;
}
if (memcmp(&buffer[0], "\xFE\xFF", 2) == 0) {
cp = CP_UTF16BE;
} else {
len = 0;
}
}
if (cp == 0) {
cp = CP_UTF8;
}
*cp_out = cp;
return len;
}
void text_buffer_read_file(TextBuffer* tb, s8 path)
{
void* file = os_open_file_for_reading(path);
if (!file) {
return;
}
usize chunk_size = os_file_size(file) + TEXT_BUFFER_ALLOC_CHUNK_BYTES / 2;
for (;;) {
c8* beg = text_buffer_allocate_gap(tb, tb->text_length, chunk_size);
usize len = os_read_file(file, beg, chunk_size);
text_buffer_commit_gap(tb, len);
if (len < chunk_size) {
break;
}
chunk_size = TEXT_BUFFER_ALLOC_CHUNK_BYTES;
}
os_close_file(file);
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
ucd_newlines_forward(chunk, 0, &tb->stats.lines, COORD_TYPE_MAX);
tb->stats.lines += 1;
}
void text_buffer_write_file(TextBuffer* tb, s8 path)
{
c8* before_gap = tb->text;
usize before_gap_len = tb->gap_off;
c8* after_gap = before_gap + tb->gap_off + tb->gap_len;
usize after_gap_len = tb->text_length - tb->gap_off;
// TODO: Write to a temp file and do an atomic rename.
void* file = os_open_file_for_writing(path);
if (!file) {
return;
}
os_write_file(file, before_gap, before_gap_len);
os_write_file(file, after_gap, after_gap_len);
os_close_file(file);
tb->dirty = false;
}
void text_buffer_selection_update(TextBuffer* tb, Point pos)
{
if (tb->selection.state == TEXT_BUFFER_SELECTION_NONE || tb->selection.state == TEXT_BUFFER_SELECTION_DONE) {
tb->selection.state = TEXT_BUFFER_SELECTION_MAYBE;
tb->selection.beg = pos;
} else {
tb->selection.state = TEXT_BUFFER_SELECTION_ACTIVE;
tb->selection.end = pos;
}
}
bool text_buffer_selection_end(TextBuffer* tb)
{
bool active = tb->selection.state == TEXT_BUFFER_SELECTION_ACTIVE;
tb->selection.state = active ? TEXT_BUFFER_SELECTION_DONE : TEXT_BUFFER_SELECTION_NONE;
return active;
}
static void text_buffer_goto_line_start(TextBuffer* tb, CoordType y)
{
const usize start_offset = tb->cursor.offset;
if (y > tb->cursor.logical_pos.y) {
while (y > tb->cursor.logical_pos.y) {
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
tb->cursor.offset += ucd_newlines_forward(chunk, 0, &tb->cursor.logical_pos.y, y);
}
if (tb->cursor.offset != tb->text_length) {
goto done;
}
// If there's no trailing newline on the last line in the file, we must go
// back to the start of the line, so that `tb->cursor.logical_pos.x == 0` holds true.
// TODO: Must debug this to make sure it works.
//__debugbreak();
y = tb->cursor.logical_pos.y - 1;
}
do {
s8 chunk = text_buffer_read_backward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
tb->cursor.offset -= chunk.len - ucd_newlines_backward(chunk, chunk.len, &tb->cursor.logical_pos.y, y);
} while (y < tb->cursor.logical_pos.y);
done:
tb->cursor.logical_pos.x = 0;
if (tb->word_wrap_columns < 0) {
// Without line wrapping it's easy: The visual line number equals the logical one.
tb->cursor.visual_pos.x = 0;
tb->cursor.visual_pos.y = tb->cursor.logical_pos.y;
} else {
// With line wrapping we need to count how many wrapped lines we've crossed.
Point pos = {};
usize offset, goal_offset;
if (start_offset < tb->cursor.offset) {
// Cursor moved down. The start position is the previous cursor position,
// and so it may not be at the start of a line.
pos.x = tb->cursor.visual_pos.x;
offset = start_offset;
goal_offset = tb->cursor.offset;
} else {
// Cursor moved up. The start position is the current offset which,
// after the code above, is at the start of a line.
offset = tb->cursor.offset;
goal_offset = start_offset;
}
CoordType delta = 0;
if (offset < goal_offset) {
while (true) {
s8 chunk = text_buffer_read_forward(tb, offset);
if (chunk.len == 0) {
break;
}
UcdMeasurement wrap;
ucd_measure_forward(chunk, 0, pos, tb->word_wrap_columns, -1, &wrap);
offset += wrap.offset;
if (offset >= goal_offset) {
break;
}
delta += 1;
}
}
if (start_offset > tb->cursor.offset) {
// Cursor moved up.
delta = -delta;
}
tb->cursor.visual_pos.x = 0;
tb->cursor.visual_pos.y += delta;
}
}
void text_buffer_reflow(TextBuffer* tb, CoordType width)
{
if (width <= 0) {
width = -1;
}
if (tb->word_wrap_columns == width) {
return;
}
const Point pos = tb->cursor.logical_pos;
tb->word_wrap_columns = width;
tb->cursor = (TextBufferCursor){};
text_buffer_cursor_move_to_logical(tb, pos);
}
usize text_buffer_cursor_move_to_logical(TextBuffer* tb, Point pos)
{
CoordType x = max(pos.x, 0);
CoordType y = max(pos.y, 0);
text_buffer_goto_line_start(tb, y);
if (tb->word_wrap_columns < 0) {
while (x > tb->cursor.logical_pos.x) {
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
UcdMeasurement m = ucd_measure_forward(chunk, 0, tb->cursor.visual_pos, -1, x - tb->cursor.logical_pos.x, NULL);
tb->cursor.offset += m.offset;
tb->cursor.logical_pos.x += m.movements;
tb->cursor.visual_pos = m.pos;
if (m.offset < chunk.len) {
break;
}
}
} else {
if (x > tb->cursor.logical_pos.x) {
// The primary loop exit condition is down below `if (tb->cursor.logical_pos.x >= x)`.
while (true) {
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
UcdMeasurement wrap;
UcdMeasurement m = ucd_measure_forward(chunk, 0, tb->cursor.visual_pos, tb->word_wrap_columns, x - tb->cursor.logical_pos.x, &wrap);
tb->cursor.offset += wrap.offset;
tb->cursor.logical_pos.x += wrap.movements;
tb->cursor.visual_pos = wrap.pos;
if (m.offset < chunk.len) {
break;
}
// Line wrap.
tb->cursor.visual_pos.x = 0;
tb->cursor.visual_pos.y += 1;
}
}
}
assert(tb->cursor.offset <= tb->text_length);
assert(tb->cursor.logical_pos.x >= 0);
assert(tb->cursor.logical_pos.y >= 0);
assert(tb->cursor.logical_pos.y < tb->stats.lines);
return tb->cursor.offset;
}
usize text_buffer_cursor_move_to_visual(TextBuffer* tb, Point pos)
{
CoordType x = max(pos.x, 0);
CoordType y = max(pos.y, 0);
if (tb->word_wrap_columns < 0) {
text_buffer_cursor_move_to_logical(tb, (Point){0, y});
while (x > tb->cursor.visual_pos.x) {
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
UcdMeasurement m = ucd_measure_forward(chunk, 0, tb->cursor.visual_pos, x, -1, NULL);
tb->cursor.offset += m.offset;
tb->cursor.logical_pos.x += m.movements;
tb->cursor.visual_pos = m.pos;
if (m.offset < chunk.len) {
break;
}
}
} else {
while (y < tb->cursor.visual_pos.y) {
text_buffer_cursor_move_to_logical(tb, (Point){0, tb->cursor.logical_pos.y - 1});
}
if (y > tb->cursor.visual_pos.y || x > tb->cursor.visual_pos.x) {
// The primary loop exit condition is down below `if (x <= tb->cursor.logical_pos.x)`.
while (true) {
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
if (chunk.len == 0) {
break;
}
CoordType column_stop = tb->word_wrap_columns;
if (tb->cursor.visual_pos.y >= y) {
// We shouldn't have sought beyond the target line.
assert(tb->cursor.visual_pos.y == y);
column_stop = x;
}
UcdMeasurement wrap;
UcdMeasurement m = ucd_measure_forward(chunk, 0, tb->cursor.visual_pos, column_stop, -1, &wrap);
tb->cursor.offset += wrap.offset;
tb->cursor.logical_pos.x += wrap.movements;
tb->cursor.visual_pos = wrap.pos;
if (m.offset < chunk.len && tb->cursor.visual_pos.y >= y) {
break;
}
// Line wrap.
tb->cursor.visual_pos.x = 0;
tb->cursor.visual_pos.y += 1;
if (m.newline) {
tb->cursor.logical_pos.x = 0;
tb->cursor.offset += ucd_newlines_forward(chunk, m.offset, &tb->cursor.logical_pos.y, tb->cursor.logical_pos.y + 1) - m.offset;
}
}
}
}
assert(tb->cursor.offset <= tb->text_length);
assert(tb->cursor.logical_pos.x >= 0);
assert(tb->cursor.logical_pos.y >= 0);
assert(tb->cursor.logical_pos.y < tb->stats.lines);
return tb->cursor.offset;
}
usize text_buffer_cursor_move_delta(TextBuffer* tb, CoordType cursor_movements)
{
if (cursor_movements < 0) {
const usize offset = tb->cursor.offset;
text_buffer_cursor_move_to_logical(tb, (Point){tb->cursor.logical_pos.x - 1, tb->cursor.logical_pos.y});
if (offset == tb->cursor.offset) {
text_buffer_cursor_move_to_logical(tb, (Point){COORD_TYPE_SAFE_MAX, tb->cursor.logical_pos.y - 1});
}
} else if (cursor_movements > 0) {
const usize offset = tb->cursor.offset;
text_buffer_cursor_move_to_logical(tb, (Point){tb->cursor.logical_pos.x + 1, tb->cursor.logical_pos.y});
if (offset == tb->cursor.offset) {
text_buffer_cursor_move_to_logical(tb, (Point){0, tb->cursor.logical_pos.y + 1});
}
}
return tb->cursor.offset;
}
usize text_buffer_extract(TextBuffer* tb, usize beg, usize end, c8* dst)
{
assert(beg <= end && end <= tb->text_length);
if (beg >= end || end > tb->text_length) {
return 0;
}
c8* buf_end = dst;
if (beg < tb->gap_off) {
// s(tart), l(ength)
usize s = beg;
usize l = min(end, tb->gap_off) - beg;
memcpy(buf_end, tb->text + s, l);
buf_end += l;
}
if (end > tb->gap_off) {
usize s = max(beg, tb->gap_off);
usize l = end - s;
memcpy(buf_end, tb->text + tb->gap_len + s, l);
buf_end += l;
}
return buf_end - dst;
}
static void text_buffer_apply_change(TextBuffer* tb, TextBufferChange* change)
{
tb->cursor = change->cursor;
// Since we'll be deleting the inserted portion, we only need to allocate the delta.
usize gap_len = max(change->removed.len, change->inserted.len) - change->inserted.len;
c8* dst = text_buffer_allocate_gap(tb, tb->cursor.offset, gap_len);
// excerpt of text_buffer_delete_right
{
usize count = change->inserted.len;
#ifndef NDEBUG
memset(tb->text + tb->gap_off + tb->gap_len, 0xDD, count);
#endif
tb->gap_len += count;
tb->text_length -= count;
}
// excerpt of text_buffer_insert
{
s8 str = change->removed;
memcpy(dst, str.beg, str.len);
text_buffer_commit_gap(tb, str.len);
TextBufferCursor cursor_before = tb->cursor;
text_buffer_cursor_move_delta(tb, -1);
usize backoff_count = cursor_before.offset - tb->cursor.offset;
s8 chunk = text_buffer_read_forward(tb, tb->cursor.offset);
chunk = s8_slice(chunk, 0, backoff_count + str.len);
UcdMeasurement after = ucd_measure_forward(chunk, 0, tb->cursor.logical_pos, COORD_TYPE_MAX, COORD_TYPE_MAX, NULL);
tb->cursor.offset += after.offset;
tb->cursor.logical_pos = after.pos;
}
}
void text_buffer_undo(TextBuffer* tb)
{
if (!tb->undo_tail) {
return;
}
TextBufferChange* change = tb->undo_tail;
tb->undo_tail = change->prev;
text_buffer_apply_change(tb, change);
s8 removed = change->removed;
s8 inserted = change->inserted;
change->cursor = tb->cursor;
change->removed = inserted;
change->inserted = removed;
}
void text_buffer_redo(TextBuffer* tb)
{
TextBufferChange* change = tb->undo_tail;
if (!change) {
change = tb->undo_base;
if (!change) {
return;
}
}
change = change->next;
if (!change) {
return;
}
TextBufferCursor cursor = tb->cursor;
tb->undo_tail = change;
text_buffer_apply_change(tb, change);
s8 removed = change->removed;
s8 inserted = change->inserted;
change->cursor = cursor;
change->removed = inserted;
change->inserted = removed;
}
void text_buffer_write(TextBuffer* tb, s8 str)
{
if (str.len == 0) {
return;
}
c8* dst = text_buffer_allocate_gap(tb, tb->cursor.offset, str.len);
memcpy(dst, str.beg, str.len);
s8 text = {tb->text, tb->gap_off + str.len, tb->gap_off + str.len};
UcdMeasurement prev_bck;
UcdMeasurement prev_fwd;
if (tb->gap_off == 0 || tb->text[tb->gap_off - 1] == '\t') {
prev_bck.offset = tb->gap_off;
prev_bck.pos = tb->cursor.logical_pos;
prev_bck.movements = 0;
prev_fwd = prev_bck;
} else {
prev_bck = ucd_measure_backward(text, tb->gap_off, tb->cursor.logical_pos, -1, 1);
prev_fwd = ucd_measure_forward(text, prev_bck.offset, prev_bck.pos, -1, 1, NULL);
}
UcdMeasurement next = ucd_measure_forward(text, prev_fwd.offset, prev_fwd.pos, -1, -1, NULL);
usize off_beg = tb->cursor.offset;
usize off_end = tb->cursor.offset;
if (tb->overtype) {
UcdMeasurement fwd = ucd_measure_forward(text_buffer_read_forward(tb, tb->cursor.offset), 0, tb->cursor.logical_pos, COORD_TYPE_MAX, next.movements, NULL);
bool combines_with_preceding = prev_fwd.offset != tb->gap_off;
off_beg = combines_with_preceding ? prev_bck.offset : prev_fwd.offset;
off_end = tb->gap_off + fwd.offset;
}
text_buffer_record_push_change(tb, off_beg, off_end, str);
text_buffer_commit_gap(tb, str.len);
tb->gap_len += off_end - off_beg;
tb->cursor.offset = next.offset;
tb->cursor.logical_pos = next.pos;
}
void text_buffer_delete(TextBuffer* tb, CoordType cursor_movements)
{
TextBufferCursor cursor_beg = tb->cursor;
text_buffer_cursor_move_delta(tb, cursor_movements);
TextBufferCursor cursor_end = tb->cursor;
if (cursor_beg.offset == cursor_end.offset) {
return;
}
if (cursor_beg.offset > cursor_end.offset) {
TextBufferCursor tmp = cursor_beg;
cursor_beg = cursor_end;
cursor_end = tmp;
}
tb->cursor = cursor_beg;
text_buffer_allocate_gap(tb, cursor_beg.offset, 0);
text_buffer_record_push_change(tb, cursor_beg.offset, cursor_end.offset, (s8){});
usize count = cursor_end.offset - cursor_beg.offset;
#ifndef NDEBUG
memset(tb->text + tb->gap_off + tb->gap_len, 0xDD, count);
#endif
tb->gap_len += count;
tb->text_length -= count;
}

96
src/buffer.h Normal file
View File

@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "arena.h"
typedef struct TextBufferStatistics {
CoordType lines;
} TextBufferStatistics;
typedef struct TextBufferCursor {
usize offset;
Point logical_pos; // in lines & graphemes; line wrapping has no influence on this
Point visual_pos; // in rows & columns, including line wrapping
} TextBufferCursor;
typedef enum TextBufferSelectionState {
TEXT_BUFFER_SELECTION_NONE,
TEXT_BUFFER_SELECTION_MAYBE,
TEXT_BUFFER_SELECTION_ACTIVE,
TEXT_BUFFER_SELECTION_DONE,
} TextBufferSelectionState;
typedef struct TextBufferSelection {
Point beg; // inclusive
Point end; // inclusive
TextBufferSelectionState state;
} TextBufferSelection;
typedef struct TextBufferChange TextBufferChange;
struct TextBufferChange {
TextBufferChange* prev;
TextBufferChange* next;
TextBufferCursor cursor;
s8 removed; // The text that was removed.
s8 inserted; // The text that was inserted.
};
typedef struct TextBufferAllocator TextBufferAllocator;
struct TextBufferAllocator {
void* (*realloc)(void* base, usize old_len, usize* new_len);
void (*free)(void* base, usize len);
};
typedef struct TextBuffer {
TextBufferAllocator allocator;
c8* main_base;
usize main_commit;
c8* text;
usize text_length; // does not account for the gap_len
usize gap_off;
usize gap_len;
void* undo_base;
TextBufferChange* undo_tail;
usize undo_usage;
usize undo_commit;
TextBufferStatistics stats;
TextBufferCursor cursor;
TextBufferSelection selection;
CoordType word_wrap_columns;
bool dirty;
bool overtype;
} TextBuffer;
TextBufferAllocator text_buffer_allocator_default();
TextBuffer* text_buffer_create(TextBufferAllocator allocator);
void text_buffer_destroy(TextBuffer* tb);
void text_buffer_read_file(TextBuffer* tb, s8 path);
void text_buffer_write_file(TextBuffer* tb, s8 path);
s8 text_buffer_read_backward(const TextBuffer* tb, usize off);
s8 text_buffer_read_forward(const TextBuffer* tb, usize off);
void text_buffer_selection_update(TextBuffer* tb, Point pos);
bool text_buffer_selection_end(TextBuffer* tb);
void text_buffer_reflow(TextBuffer* tb, CoordType width);
usize text_buffer_cursor_move_to_logical(TextBuffer* tb, Point pos);
usize text_buffer_cursor_move_to_visual(TextBuffer* tb, Point pos);
usize text_buffer_cursor_move_delta(TextBuffer* tb, CoordType cursor_movements);
usize text_buffer_extract(TextBuffer* tb, usize beg, usize end, c8* dst);
void text_buffer_undo(TextBuffer* tb);
void text_buffer_redo(TextBuffer* tb);
void text_buffer_write(TextBuffer* tb, s8 str);
void text_buffer_delete(TextBuffer* tb, CoordType cursor_movements);

283
src/helpers.c Normal file
View File

@ -0,0 +1,283 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "helpers.h"
bool rect_is_empty(Rect rect)
{
return rect.left >= rect.right || rect.top >= rect.bottom;
}
bool rect_contains(Rect rect, Point point)
{
return point.x >= rect.left && point.x < rect.right && point.y >= rect.top && point.y < rect.bottom;
}
Rect rect_intersect(Rect lhs, Rect rhs)
{
#if 1
CoordType l = max(lhs.left, rhs.left);
CoordType t = max(lhs.top, rhs.top);
CoordType r = min(lhs.right, rhs.right);
CoordType b = min(lhs.bottom, rhs.bottom);
// Ensure that the size is non-negative. This avoids bugs,
// because some height/width is negative all of a sudden.
r = max(l, r);
b = max(t, b);
return (Rect){l, t, r, b};
#else
// I wrote some SSE2 code for fun. It's a lot faster and more compact than the scalar code.
// I've tested it a little bit, and I believe it's correct. It's left disabled until I'm at
// a point were vectorization can be justified (= the editor not being complete trash).
__m128i a = _mm_loadu_si128((__m128i*)&lhs);
__m128i b = _mm_loadu_si128((__m128i*)&rhs);
// Compute the min of left/top and max of right/bottom.
__m128i lt = _mm_max_epi32(a, b);
__m128i rb = _mm_min_epi32(a, b);
// Duplicate the min() of left/top into the upper half and...
lt = _mm_shuffle_epi32(lt, _MM_SHUFFLE(1, 0, 1, 0));
// ...ensure that the size is non-negative by ensuring that
// the upper half of `rb` is at least as large as `lt`.
rb = _mm_max_epi32(lt, rb);
// Combine the lower half `lt` (max of left/top) and
// the upper half of `rb` (min of right/bottom).
__m128i res = _mm_blend_epi16(lt, rb, 0xf0);
Rect result;
_mm_storeu_si128((__m128i*)&result, res);
return result;
#endif
}
s8 s8_from_ptr(const c8* ptr)
{
usize len = strlen((const char*)ptr);
return (s8){(c8*)ptr, len, len};
}
s8 s8_slice(s8 str, usize beg, usize end)
{
end = min(end, str.len);
beg = min(beg, end);
return (s8){str.beg + beg, end - beg, end - beg};
}
bool s8_starts_with(s8 str, s8 prefix)
{
return str.len >= prefix.len && memcmp(str.beg, prefix.beg, prefix.len) == 0;
}
void s8_transform_lowercase_ascii(s8 str)
{
for (usize i = 0; i < str.len; ++i) {
if (str.beg[i] >= 'A' && str.beg[i] <= 'Z') {
str.beg[i] += 'a' - 'A';
}
}
}
usize s8_find(s8 str, usize off, c8 ch)
{
for (; off < str.len; ++off) {
if (str.beg[off] == ch) {
return off;
}
}
return str.len;
}
u64 s8_to_u64(s8 str, int base) {
c8* ptr = str.beg;
const c8* end = ptr + str.len;
u64 accumulator = 0;
u64 base64 = base;
if (base <= 0)
{
base64 = 10;
if (ptr != end && *ptr == '0')
{
base64 = 8;
ptr += 1;
if (ptr != end && (*ptr == 'x' || *ptr == 'X'))
{
base64 = 16;
ptr += 1;
}
}
}
if (ptr == end)
{
return 0;
}
for (;;)
{
u64 value = 0;
if (*ptr >= '0' && *ptr <= '9')
{
value = *ptr - '0';
}
else if (*ptr >= 'A' && *ptr <= 'F')
{
value = *ptr - 'A' + 10;
}
else if (*ptr >= 'a' && *ptr <= 'f')
{
value = *ptr - 'a' + 10;
}
else
{
return 0;
}
const u64 acc = accumulator * base64 + value;
if (acc < accumulator)
{
return 0;
}
accumulator = acc;
ptr += 1;
if (ptr == end)
{
return accumulator;
}
}
}
static u64 wyr3(const u8* p, usize k)
{
return ((u64)p[0] << 16) | ((u64)p[k >> 1] << 8) | p[k - 1];
}
static u64 wyr4(const u8* p)
{
uint32_t v;
memcpy(&v, p, 4);
return v;
}
static u64 wyr8(const u8* p)
{
u64 v;
memcpy(&v, p, 8);
return v;
}
static u64 wymix(u64 lhs, u64 rhs)
{
#if os_UNIX
__uint128_t r = (__uint128_t)lhs * (__uint128_t)rhs;
return (u64)(r >> 64) ^ (u64)r;
#elif PLATFORM_X64
extern u64 _umul128(u64 Multiplier, u64 Multiplicand, u64 * HighProduct);
u64 hi = 0;
u64 lo = _umul128(lhs, rhs, &hi);
return lo ^ hi;
#elif os_ARM64
extern u64 __umulh(u64 a, u64 b);
const u64 lo = lhs * rhs;
const u64 hi = __umulh(lhs, rhs);
return lo ^ hi;
#endif
}
// The venerable wyhash hash function. It's fast and has good statistical properties.
// It's in the public domain.
u64 hash(u64 seed, void* data, usize len)
{
static const u64 s0 = 0xa0761d6478bd642f;
static const u64 s1 = 0xe7037ed1a0b428db;
static const u64 s2 = 0x8ebc6af09c88c6e3;
static const u64 s3 = 0x589965cc75374cc3;
const u8* p = data;
seed ^= s0;
u64 a;
u64 b;
if (len <= 16) {
if (len >= 4) {
a = (wyr4(p) << 32) | wyr4(p + ((len >> 3) << 2));
b = (wyr4(p + len - 4) << 32) | wyr4(p + len - 4 - ((len >> 3) << 2));
} else if (len > 0) {
a = wyr3(p, len);
b = 0;
} else {
a = b = 0;
}
} else {
usize i = len;
if (i > 48) {
u64 seed1 = seed;
u64 seed2 = seed;
do {
seed = wymix(wyr8(p) ^ s1, wyr8(p + 8) ^ seed);
seed1 = wymix(wyr8(p + 16) ^ s2, wyr8(p + 24) ^ seed1);
seed2 = wymix(wyr8(p + 32) ^ s3, wyr8(p + 40) ^ seed2);
p += 48;
i -= 48;
} while (i > 48);
seed ^= seed1 ^ seed2;
}
while (i > 16) {
seed = wymix(wyr8(p) ^ s1, wyr8(p + 8) ^ seed);
i -= 16;
p += 16;
}
a = wyr8(p + i - 16);
b = wyr8(p + i - 8);
}
return wymix(s1 ^ len, wymix(a ^ s1, b ^ seed));
}
u64 hash_s8(u64 seed, s8 str)
{
return hash(seed, str.beg, str.len);
}
u64 u64_log10(u64 v)
{
// This implements https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10 but with lzcnt for log2.
static const u64 powers_of_10[] = {
0u,
10u,
100u,
1000u,
10000u,
100000u,
1000000u,
10000000u,
100000000u,
1000000000u,
10000000000u,
100000000000u,
1000000000000u,
10000000000000u,
100000000000000u,
1000000000000000u,
10000000000000000u,
100000000000000000u,
1000000000000000000u,
10000000000000000000u,
};
unsigned long index;
#ifdef _MSC_VER
_BitScanReverse64(&index, v | 1);
#else
index = 64 - __builtin_clzll(v | 1);
#endif
const u64 t = (index + 1) * 1233 >> 12;
return t - (v < powers_of_10[t]);
}

247
src/helpers.h Normal file
View File

@ -0,0 +1,247 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#ifndef unreachable
#define unreachable() __assume(false)
#endif
#ifdef _MSC_VER
#include <intrin.h>
#define os_WIN32 1
#define attribute_forceinline __forceinline
#define attribute_noinline __declspec(noinline)
#else
#define os_UNIX 1
#define attribute_forceinline __attribute__((always_inline)) inline
#define attribute_noinline __attribute__((noinline))
#endif
#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))
#define PLATFORM_X64 1
#elif defined(__aarch64__) || (defined(_M_ARM64) || defined(_M_ARM64EC))
#define PLATFORM_ARM64 1
#else
#error "Unsupported platform"
#endif
typedef int8_t i8;
typedef uint8_t u8;
typedef int16_t i16;
typedef uint16_t u16;
typedef int32_t i32;
typedef uint32_t u32;
typedef int64_t i64;
typedef uint64_t u64;
typedef ptrdiff_t isize;
typedef size_t usize;
typedef float f32;
typedef double f64;
#define USIZE_MAX ((usize) - 1)
typedef i32 CoordType;
#define COORD_TYPE_MIN (-2147483647 - 1)
#define COORD_TYPE_MAX 2147483647
#define COORD_TYPE_SAFE_MIN (-32767 - 1)
#define COORD_TYPE_SAFE_MAX 32767
#define array_size(a) (sizeof(a) / sizeof((a)[0]))
// clang-format off
attribute_forceinline i32 min_i32(i32 a, i32 b) { return a < b ? a : b; }
attribute_forceinline u32 min_u32(u32 a, u32 b) { return a < b ? a : b; }
attribute_forceinline i64 min_i64(i64 a, i64 b) { return a < b ? a : b; }
attribute_forceinline u64 min_u64(u64 a, u64 b) { return a < b ? a : b; }
attribute_forceinline f32 min_f32(f32 a, f32 b) { return a < b ? a : b; }
attribute_forceinline f64 min_f64(f64 a, f64 b) { return a < b ? a : b; }
attribute_forceinline i32 max_i32(i32 a, i32 b) { return a > b ? a : b; }
attribute_forceinline u32 max_u32(u32 a, u32 b) { return a > b ? a : b; }
attribute_forceinline i64 max_i64(i64 a, i64 b) { return a > b ? a : b; }
attribute_forceinline u64 max_u64(u64 a, u64 b) { return a > b ? a : b; }
attribute_forceinline f32 max_f32(f32 a, f32 b) { return a > b ? a : b; }
attribute_forceinline f64 max_f64(f64 a, f64 b) { return a > b ? a : b; }
attribute_forceinline i32 clamp_i32(i32 v, i32 lo, i32 hi) { return v < lo ? lo : (v > hi ? hi : v); }
attribute_forceinline u32 clamp_u32(u32 v, u32 lo, u32 hi) { return v < lo ? lo : (v > hi ? hi : v); }
attribute_forceinline i64 clamp_i64(i64 v, i64 lo, i64 hi) { return v < lo ? lo : (v > hi ? hi : v); }
attribute_forceinline u64 clamp_u64(u64 v, u64 lo, u64 hi) { return v < lo ? lo : (v > hi ? hi : v); }
attribute_forceinline f32 clamp_f32(f32 v, f32 lo, f32 hi) { return v < lo ? lo : (v > hi ? hi : v); }
attribute_forceinline f64 clamp_f64(f64 v, f64 lo, f64 hi) { return v < lo ? lo : (v > hi ? hi : v); }
#define min(a, b) _Generic((a), i32 : min_i32, u32 : min_u32, i64 : min_i64, u64 : min_u64, f32 : min_f32, f64 : min_f64)(a, b)
#define max(a, b) _Generic((a), i32 : max_i32, u32 : max_u32, i64 : max_i64, u64 : max_u64, f32 : max_f32, f64 : max_f64)(a, b)
#define clamp(v, lo, hi) _Generic((v), i32 : clamp_i32, u32 : clamp_u32, i64 : clamp_i64, u64 : clamp_u64, f32 : clamp_f32, f64 : clamp_f64)(v, lo, hi)
// clang-format on
typedef struct Point {
CoordType x;
CoordType y;
} Point;
typedef struct Size {
CoordType width;
CoordType height;
} Size;
typedef struct Rect {
CoordType left;
CoordType top;
CoordType right;
CoordType bottom;
} Rect;
bool rect_is_empty(Rect rect);
bool rect_contains(Rect rect, Point point);
Rect rect_intersect(Rect a, Rect b);
typedef u8 c8; // UTF-8 code unit
typedef u16 c16; // UTF-16 code unit
// UTF-8 text
typedef struct s8 {
c8* beg;
usize len;
usize cap;
} s8;
attribute_forceinline s8 S(const char* str)
{
usize len = strlen(str);
return (s8){(c8*)str, len, len};
}
s8 s8_from_ptr(const c8* ptr);
s8 s8_slice(s8 str, usize beg, usize end);
bool s8_starts_with(s8 str, s8 prefix);
// NOTE: Calling s8_transform_lowercase_ascii on non-ASCII strings will result in garbage.
void s8_transform_lowercase_ascii(s8 str);
usize s8_find(s8 str, usize off, c8 ch);
u64 s8_to_u64(s8 str, int base);
u64 hash(u64 seed, void* data, usize len);
u64 hash_s8(u64 seed, s8 str);
u64 u64_log10(u64 v);
#define DOUBLY_LINKED_ENTRY(type) \
type* prev; \
type* next;
#define DOUBLY_LINKED_INIT(list, field) \
(list)->field.prev = &(list)->field; \
(list)->field.next = &(list)->field;
#define DOUBLY_LINKED_PUSH_TAIL(list, field, item) \
do { \
__typeof__(list) prev = (list)->field.prev; \
entry->field.next = (list); \
entry->field.prev = prev; \
prev->field.next = item; \
(list)->field.prev = entry; \
} while (0)
#define DOUBLY_LINKED_INSERT_TAIL(head, obj, field) \
(obj)->field.Flink = (head); \
(obj)->field.Blink = (head)->field.Blink; \
(head)->field.Blink->Flink = (obj); \
(head)->field.Blink = (obj);
// Reference code for single/doubly linked lists with sentinels.
// The problem with that is that it requires ugly offsetof() casts to get the owning structure.
#if 0
typedef struct SinglyLinkedEntry {
struct SinglyLinkedEntry* next;
} SinglyLinkedEntry;
attribute_forceinline SinglyLinkedEntry* singly_linked_pop_head(SinglyLinkedEntry* list)
{
SinglyLinkedEntry* entry = list->next;
if (entry != NULL) {
list->next = entry->next;
}
return entry;
}
attribute_forceinline void singly_linked_push_head(SinglyLinkedEntry* list, SinglyLinkedEntry* entry)
{
entry->next = list->next;
list->next = entry;
}
typedef struct DoublyLinkedEntry {
struct DoublyLinkedEntry* Flink;
struct DoublyLinkedEntry* Blink;
} DoublyLinkedEntry;
attribute_forceinline void doubly_linked_init(DoublyLinkedEntry* list)
{
list->Flink = list->Blink = list;
}
bool attribute_forceinline doubly_linked_is_empty(const DoublyLinkedEntry* list)
{
return list->Flink == list;
}
attribute_forceinline void doubly_linked_remove_entry(DoublyLinkedEntry* entry)
{
DoublyLinkedEntry* Flink = entry->Flink;
DoublyLinkedEntry* Blink = entry->Blink;
Blink->Flink = Flink;
Flink->Blink = Blink;
}
attribute_forceinline DoublyLinkedEntry* doubly_linked_remove_head(DoublyLinkedEntry* list)
{
DoublyLinkedEntry* entry = list->Flink;
DoublyLinkedEntry* Flink = entry->Flink;
list->Flink = Flink;
Flink->Blink = list;
return entry;
}
attribute_forceinline DoublyLinkedEntry* doubly_linked_remove_tail(DoublyLinkedEntry* list)
{
DoublyLinkedEntry* entry = list->Blink;
DoublyLinkedEntry* Blink = entry->Blink;
list->Blink = Blink;
Blink->Flink = list;
return entry;
}
attribute_forceinline void doubly_linked_push_head(DoublyLinkedEntry* list, DoublyLinkedEntry* entry)
{
DoublyLinkedEntry* Flink = list->Flink;
entry->Flink = Flink;
entry->Blink = list;
Flink->Blink = entry;
list->Flink = entry;
}
attribute_forceinline void doubly_linked_push_tail(DoublyLinkedEntry* list, DoublyLinkedEntry* entry)
{
DoublyLinkedEntry* Blink = list->Blink;
entry->Flink = list;
entry->Blink = Blink;
Blink->Flink = entry;
list->Blink = entry;
}
attribute_forceinline void doubly_linked_append_list(DoublyLinkedEntry* list, DoublyLinkedEntry* append)
{
DoublyLinkedEntry* end = list->Blink;
list->Blink->Flink = append;
list->Blink = append->Blink;
append->Blink->Flink = list;
append->Blink = end;
}
#endif

431
src/icu.c Normal file
View File

@ -0,0 +1,431 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "icu.h"
#include <icu.h>
#include "buffer.h"
#include "os.h"
static int s_icu_ok = -1;
static UText*(U_EXPORT2* fn_utext_close)(UText* ut);
static UText*(U_EXPORT2* fn_utext_setup)(UText* ut, int32_t extraSpace, UErrorCode* status);
static int32_t(U_EXPORT2* fn_u_strlen)(const UChar* s);
static char*(U_EXPORT2* fn_u_strToUTF8WithSub)(char* dest, int32_t destCapacity, int32_t* pDestLength, const UChar* src, int32_t srcLength, UChar32 subchar, int32_t* pNumSubstitutions, UErrorCode* pErrorCode);
static UChar*(U_EXPORT2* fn_u_strFromUTF8WithSub)(UChar* dest, int32_t destCapacity, int32_t* pDestLength, const char* src, int32_t srcLength, UChar32 subchar, int32_t* pNumSubstitutions, UErrorCode* pErrorCode);
static UConverter*(U_EXPORT2* fn_ucnv_open)(const char* converterName, UErrorCode* err);
static void(U_EXPORT2* fn_ucnv_close)(UConverter* converter);
static const char*(U_EXPORT2* fn_ucnv_detectUnicodeSignature)(const char* source, int32_t sourceLength, int32_t* signatureLength, UErrorCode* pErrorCode);
static void(U_EXPORT2* fn_ucnv_convertEx)(UConverter* targetCnv, UConverter* sourceCnv, char** target, const char* targetLimit, const char** source, const char* sourceLimit, UChar* pivotStart, UChar** pivotSource, UChar** pivotTarget, const UChar* pivotLimit, UBool reset, UBool flush, UErrorCode* pErrorCode);
/**
* Function type declaration for UText.clone().
*
* clone a UText. Much like opening a UText where the source text is itself
* another UText.
*
* A deep clone will copy both the UText data structures and the underlying text.
* The original and cloned UText will operate completely independently; modifications
* made to the text in one will not effect the other. Text providers are not
* required to support deep clones. The user of clone() must check the status return
* and be prepared to handle failures.
*
* A shallow clone replicates only the UText data structures; it does not make
* a copy of the underlying text. Shallow clones can be used as an efficient way to
* have multiple iterators active in a single text string that is not being
* modified.
*
* A shallow clone operation must not fail except for truly exceptional conditions such
* as memory allocation failures.
*
* A UText and its clone may be safely concurrently accessed by separate threads.
* This is true for both shallow and deep clones.
* It is the responsibility of the Text Provider to ensure that this thread safety
* constraint is met.
*
* @param dest A UText struct to be filled in with the result of the clone operation,
* or NULL if the clone function should heap-allocate a new UText struct.
* @param src The UText to be cloned.
* @param deep true to request a deep clone, false for a shallow clone.
* @param status Errors are returned here. For deep clones, U_UNSUPPORTED_ERROR
* should be returned if the text provider is unable to clone the
* original text.
* @return The newly created clone, or NULL if the clone operation failed.
*
* @stable ICU 3.4
*/
static UText* U_CALLCONV icu_text_buffer_UTextClone(UText* dest, const UText* src, UBool deep, UErrorCode* status)
{
if (U_FAILURE(*status)) {
return NULL;
}
if (deep) {
*status = U_UNSUPPORTED_ERROR;
return dest;
}
dest = fn_utext_setup(dest, 4096, status);
if (U_FAILURE(*status)) {
return NULL;
}
dest->providerProperties = src->providerProperties;
dest->chunkNativeLimit = src->chunkNativeLimit;
dest->extraSize = src->extraSize;
dest->nativeIndexingLimit = src->nativeIndexingLimit;
dest->chunkNativeStart = src->chunkNativeStart;
dest->chunkOffset = src->chunkOffset;
dest->chunkLength = src->chunkLength;
dest->chunkContents = dest->pExtra;
dest->pFuncs = src->pFuncs;
dest->context = src->context;
dest->p = src->p;
dest->q = src->q;
dest->r = src->r;
dest->a = src->a;
dest->b = src->b;
dest->c = src->c;
memcpy(dest->pExtra, src->pExtra, 4096);
return dest;
}
/**
* Function type declaration for UText.nativeLength().
*
* @param ut the UText to get the length of.
* @return the length, in the native units of the original text string.
* @see UText
* @stable ICU 3.4
*/
static int64_t U_CALLCONV icu_text_buffer_UTextNativeLength(UText* ut)
{
TextBuffer* tb = (TextBuffer*)ut->context;
return tb->text_length;
}
/**
* Function type declaration for UText.access(). Get the description of the text chunk
* containing the text at a requested native index. The UText's iteration
* position will be left at the requested index. If the index is out
* of bounds, the iteration position will be left at the start or end
* of the string, as appropriate.
*
* Chunks must begin and end on code point boundaries. A single code point
* comprised of multiple storage units must never span a chunk boundary.
*
*
* @param ut the UText being accessed.
* @param nativeIndex Requested index of the text to be accessed.
* @param forward If true, then the returned chunk must contain text
* starting from the index, so that start<=index<limit.
* If false, then the returned chunk must contain text
* before the index, so that start<index<=limit.
* @return True if the requested index could be accessed. The chunk
* will contain the requested text.
* False value if a chunk cannot be accessed
* (the requested index is out of bounds).
*
* @see UText
* @stable ICU 3.4
*/
static UBool U_CALLCONV icu_text_buffer_UTextAccess(UText* ut, int64_t nativeIndex, UBool forward)
{
TextBuffer* tb = (TextBuffer*)ut->context;
i64 index_contained = nativeIndex;
if (!forward) {
index_contained -= 1;
}
if (index_contained < 0 || (usize)index_contained >= tb->text_length) {
return false;
}
if (index_contained >= ut->chunkNativeStart && index_contained < ut->chunkNativeLimit) {
assert(false);
}
UErrorCode status = U_ZERO_ERROR;
s8 text;
i64 native_start;
i64 native_limit;
if (forward) {
text = text_buffer_read_forward(tb, nativeIndex);
text = s8_slice(text, 0, 2048);
native_start = nativeIndex;
native_limit = nativeIndex + text.len;
} else {
text = text_buffer_read_backward(tb, nativeIndex);
text = s8_slice(text, text.len - min(text.len, 2048), text.len);
native_start = nativeIndex - text.len;
native_limit = nativeIndex;
}
// The utf16_buffer is 4096 bytes long = 2048 UChars.
// The worst case scenario is that the text is ASCII (1 UChars per byte),
// which means we can safely convert 2048 bytes to 2048 UChars.
UChar* utf16_buffer = ut->pExtra;
i32 utf16_buffer_len = 0;
fn_u_strFromUTF8WithSub(utf16_buffer, 2048, &utf16_buffer_len, (const char*)text.beg, (i32)text.len, 0xfffd, NULL, &status);
if (U_FAILURE(status)) {
assert(false);
return false;
}
ut->chunkContents = utf16_buffer;
ut->chunkLength = utf16_buffer_len;
ut->chunkOffset = forward ? 0 : utf16_buffer_len;
ut->chunkNativeStart = native_start;
ut->chunkNativeLimit = native_limit;
ut->p = text.beg;
ut->a = text.len;
return true;
}
static int32_t U_CALLCONV icu_text_buffer_UTextExtract(UText* ut, int64_t nativeStart, int64_t nativeLimit, UChar* dest, int32_t destCapacity, UErrorCode* status)
{
assert(false);
return 0;
}
/**
* Function type declaration for UText.replace().
*
* Replace a range of the original text with a replacement text.
*
* Leaves the current iteration position at the position following the
* newly inserted replacement text.
*
* This function need only be implemented on UText types that support writing.
*
* When using this function, there should be only a single UText opened onto the
* underlying native text string. The function is responsible for updating the
* text chunk within the UText to reflect the updated iteration position,
* taking into account any changes to the underlying string's structure caused
* by the replace operation.
*
* @param ut the UText representing the text to be operated on.
* @param nativeStart the index of the start of the region to be replaced
* @param nativeLimit the index of the character following the region to be replaced.
* @param replacementText pointer to the replacement text
* @param replacmentLength length of the replacement text in UChars, or -1 if the text is NUL terminated.
* @param status receives any error status. Possible errors include
* U_NO_WRITE_PERMISSION
*
* @return The signed number of (native) storage units by which
* the length of the text expanded or contracted.
*
* @stable ICU 3.4
*/
static int32_t U_CALLCONV icu_text_buffer_UTextReplace(UText* ut, int64_t nativeStart, int64_t nativeLimit, const UChar* replacementText, int32_t replacmentLength, UErrorCode* status)
{
TextBuffer* tb = (TextBuffer*)ut->context;
if (nativeStart < 0 || (usize)nativeStart > tb->text_length) {
return 0;
}
if (nativeLimit < nativeStart) {
return 0;
}
if (replacmentLength == -1) {
replacmentLength = fn_u_strlen(replacementText);
}
if ((usize)nativeLimit > tb->text_length) {
nativeLimit = tb->text_length;
}
if (nativeStart == nativeLimit && replacmentLength == 0) {
return 0;
}
assert(false); // TODO: Implement this function.
return 0;
}
/**
* Function type declaration for UText.mapOffsetToNative().
* Map from the current UChar offset within the current text chunk to
* the corresponding native index in the original source text.
*
* This is required only for text providers that do not use native UTF-16 indexes.
*
* @param ut the UText.
* @return Absolute (native) index corresponding to chunkOffset in the current chunk.
* The returned native index should always be to a code point boundary.
*
* @stable ICU 3.4
*/
static int64_t U_CALLCONV icu_text_buffer_UTextMapOffsetToNative(const UText* ut)
{
const c8* beg = ut->p;
i64 len = ut->a;
i64 native = 0;
i32 offset = 0;
while (offset < ut->chunkOffset) {
u32 c;
U8_NEXT_OR_FFFD(beg, native, len, c);
offset += c >= 0x10000 ? 2 : 1;
}
assert(offset == ut->chunkOffset);
return native + ut->chunkNativeStart;
}
/**
* Function type declaration for UText.mapIndexToUTF16().
* Map from a native index to a UChar offset within a text chunk.
* Behavior is undefined if the native index does not fall within the
* current chunk.
*
* This function is required only for text providers that do not use native UTF-16 indexes.
*
* @param ut The UText containing the text chunk.
* @param nativeIndex Absolute (native) text index, chunk->start<=index<=chunk->limit.
* @return Chunk-relative UTF-16 offset corresponding to the specified native
* index.
*
* @stable ICU 3.4
*/
static int32_t U_CALLCONV icu_text_buffer_UTextMapNativeIndexToUTF16(const UText* ut, int64_t nativeIndex)
{
i32 length;
UErrorCode status = U_ZERO_ERROR;
i32 off = (i32)(nativeIndex - ut->chunkNativeStart);
assert(off >= 0);
fn_u_strFromUTF8WithSub(NULL, 0, &length, ut->p, off, 0xfffd, NULL, &status);
return length;
}
static const UTextFuncs s_text_funcs = {
.tableSize = sizeof(UTextFuncs),
.clone = icu_text_buffer_UTextClone,
.nativeLength = icu_text_buffer_UTextNativeLength,
.access = icu_text_buffer_UTextAccess,
.extract = icu_text_buffer_UTextExtract,
.replace = icu_text_buffer_UTextReplace,
.mapOffsetToNative = icu_text_buffer_UTextMapOffsetToNative,
.mapNativeIndexToUTF16 = icu_text_buffer_UTextMapNativeIndexToUTF16,
};
void icu_init()
{
if (s_icu_ok != -1) {
return;
}
void* icu = os_load_library("icuuc.dll");
if (icu == NULL) {
return;
}
s_icu_ok = 1;
OS_SUPPRESS_GET_PROC_NAGGING_BEGIN
fn_utext_close = os_get_proc_address(icu, "utext_close");
s_icu_ok &= fn_utext_close != NULL;
fn_utext_setup = os_get_proc_address(icu, "utext_setup");
s_icu_ok &= fn_utext_setup != NULL;
fn_u_strlen = os_get_proc_address(icu, "u_strlen");
s_icu_ok &= fn_u_strlen != NULL;
fn_u_strToUTF8WithSub = os_get_proc_address(icu, "u_strToUTF8WithSub");
s_icu_ok &= fn_u_strToUTF8WithSub != NULL;
fn_u_strFromUTF8WithSub = os_get_proc_address(icu, "u_strFromUTF8WithSub");
s_icu_ok &= fn_u_strFromUTF8WithSub != NULL;
fn_ucnv_open = os_get_proc_address(icu, "ucnv_open");
s_icu_ok &= fn_ucnv_open != NULL;
fn_ucnv_close = os_get_proc_address(icu, "ucnv_close");
s_icu_ok &= fn_ucnv_close != NULL;
fn_ucnv_detectUnicodeSignature = os_get_proc_address(icu, "ucnv_detectUnicodeSignature");
s_icu_ok &= fn_ucnv_detectUnicodeSignature != NULL;
fn_ucnv_convertEx = os_get_proc_address(icu, "ucnv_convertEx");
s_icu_ok &= fn_ucnv_convertEx != NULL;
OS_SUPPRESS_GET_PROC_NAGGING_END
}
UText* text_buffer_utext(TextBuffer* tb)
{
icu_init();
if (!s_icu_ok) {
}
UErrorCode status = U_ZERO_ERROR;
UText* ut = fn_utext_setup(NULL, 4096, &status);
if (U_FAILURE(status)) {
return NULL;
}
ut->providerProperties = 0; // TODO: (1 << UTEXT_PROVIDER_WRITABLE)
ut->pFuncs = &s_text_funcs;
ut->context = tb;
return ut;
}
void text_buffer_utext_close(UText* ut)
{
fn_utext_close(ut);
}
int read(char* buffer, usize length) { return -1; }
void write(const char* buffer, usize length) {}
const char* detectUnicodeSignature(const char* source, int32_t length)
{
icu_init();
UErrorCode status = U_ZERO_ERROR;
const char* encoding = fn_ucnv_detectUnicodeSignature(source, length, NULL, &status);
return U_SUCCESS(status) && encoding ? encoding : "UTF-8";
}
void convertToUTF8(const char* encoding)
{
icu_init();
UErrorCode errorCode = U_ZERO_ERROR;
UConverter* sourceCnv = fn_ucnv_open(encoding, &errorCode);
UConverter* targetCnv = fn_ucnv_open("UTF-8", &errorCode);
if (U_FAILURE(errorCode)) {
goto cleanup;
}
char sourceBuffer[1024];
char targetBuffer[2048];
UChar pivotBuffer[1024];
const char* source;
char* target;
UChar* pivotSource = pivotBuffer;
UChar* pivotTarget = pivotBuffer;
const UChar* pivotLimit = pivotBuffer + 1024;
bool reset = true;
for (;;) {
int bytesRead = read(sourceBuffer, sizeof(sourceBuffer));
source = sourceBuffer;
const char* sourceLimit = sourceBuffer + bytesRead;
target = targetBuffer;
char* targetLimit = targetBuffer + sizeof(targetBuffer);
fn_ucnv_convertEx(targetCnv, sourceCnv, &target, targetLimit, &source, sourceLimit, pivotBuffer, &pivotSource, &pivotTarget, pivotLimit, reset, bytesRead <= 0, &errorCode);
if (U_FAILURE(errorCode)) {
break;
}
// TODO: Skip initial U+FEFF BOM?
write(targetBuffer, target - targetBuffer);
reset = false;
if (bytesRead == 0) {
break;
}
}
cleanup:
fn_ucnv_close(sourceCnv);
fn_ucnv_close(targetCnv);
}

14
src/icu.h Normal file
View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
struct UText;
typedef struct UText UText;
struct TextBuffer;
typedef struct TextBuffer TextBuffer;
void icu_init();
//UText* text_buffer_utext(TextBuffer* tb);
void text_buffer_utext_close(UText* ut);

203
src/input.c Normal file
View File

@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "input.h"
UiInput get_next_ui_input(VtParserState* vt_parser_state, s8* input)
{
UiInput result = {};
c8* it = input->beg;
c8* end = input->beg + input->len;
while (it != end) {
it = vt_parse_next_token(vt_parser_state, it, end);
switch (vt_parser_state->kind) {
case VT_TOKEN_KIND_TEXT:
result.type = UI_INPUT_TEXT;
result.text = vt_parser_state->text;
goto done;
case VT_TOKEN_KIND_CTRL:
switch (vt_parser_state->ctrl) {
case VK_NULL:
case VK_TAB:
case VK_RETURN:
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = vt_parser_state->ctrl;
goto done;
case '\x01':
case '\x02':
case '\x03':
case '\x04':
case '\x05':
case '\x06':
case '\x07':
case '\x08':
// case '\x09': // VK_TAB
case '\x0a':
case '\x0b':
case '\x0c':
// case '\x0d': // VK_RETURN
case '\x0e':
case '\x0f':
case '\x10':
case '\x11':
case '\x12':
case '\x13':
case '\x14':
case '\x15':
case '\x16':
case '\x17':
case '\x18':
case '\x19':
case '\x1a':
// Ctrl+A-Z with a few exceptions, because e.g. Ctrl+I is VK_TAB.
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = (UiInputKeyboardKey)(vt_parser_state->ctrl | 0b1000000);
result.keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
goto done;
case '\x7f':
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = VK_BACK;
goto done;
default:
assert(false);
break;
}
break;
case VT_TOKEN_KIND_ESC:
if (vt_parser_state->esc >= ' ' && vt_parser_state->esc <= '~') {
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = vt_parser_state->esc;
result.keyboard.modifiers = KEYBOARD_MODIFIER_ALT;
goto done;
}
assert(false);
break;
case VT_TOKEN_KIND_SS3:
if (vt_parser_state->ss3 >= 'P' && vt_parser_state->ss3 <= 'S') {
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = vt_parser_state->ss3 - 'P' + VK_F1;
goto done;
}
assert(false);
break;
case VT_TOKEN_KIND_CSI:
if (vt_parser_state->csi.final_byte <= 'H') {
static const u8 lut[] = {
['A' - 'A'] = VK_UP,
['B' - 'A'] = VK_DOWN,
['C' - 'A'] = VK_RIGHT,
['D' - 'A'] = VK_LEFT,
['F' - 'A'] = VK_END,
['H' - 'A'] = VK_HOME,
};
u8 vk = lut[vt_parser_state->csi.final_byte - 'A'];
if (vk) {
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = vk;
goto parse_modifiers;
}
break;
}
switch (vt_parser_state->csi.final_byte) {
case '~': {
static const u8 lut[] = {
[1] = VK_HOME,
[2] = VK_INSERT,
[3] = VK_DELETE,
[4] = VK_END,
[5] = VK_PRIOR,
[6] = VK_NEXT,
[15] = VK_F5,
[17] = VK_F6,
[18] = VK_F7,
[19] = VK_F8,
[20] = VK_F9,
[21] = VK_F10,
[23] = VK_F11,
[24] = VK_F12,
[25] = VK_F13,
[26] = VK_F14,
[28] = VK_F15,
[29] = VK_F16,
[31] = VK_F17,
[32] = VK_F18,
[33] = VK_F19,
[34] = VK_F20,
};
i32 p0 = vt_parser_state->csi.params[0];
if (p0 <= array_size(lut)) {
u8 vk = lut[p0];
if (vk) {
result.type = UI_INPUT_KEYBOARD;
result.keyboard.key = vk;
goto parse_modifiers;
}
}
break;
}
case 'm':
case 'M':
if (vt_parser_state->csi.private_byte == '<') {
i32 btn = vt_parser_state->csi.params[0];
result.type = UI_INPUT_MOUSE;
result.mouse.action = MOUSE_ACTION_NONE;
if (btn & 0x40) {
result.mouse.action = MOUSE_ACTION_SCROLL;
result.mouse.scroll.y += btn & 0x01 ? 3 : -3;
} else if (vt_parser_state->csi.final_byte == 'M') {
MouseAction actions[] = {MOUSE_ACTION_LEFT, MOUSE_ACTION_MIDDLE, MOUSE_ACTION_RIGHT, MOUSE_ACTION_NONE};
result.mouse.action = actions[btn & 0x03];
}
result.mouse.modifier = KEYBOARD_MODIFIER_NONE;
result.mouse.modifier |= btn & 0x04 ? KEYBOARD_MODIFIER_SHIFT : 0;
result.mouse.modifier |= btn & 0x08 ? KEYBOARD_MODIFIER_ALT : 0;
result.mouse.modifier |= btn & 0x10 ? KEYBOARD_MODIFIER_CTRL : 0;
result.mouse.position.x = vt_parser_state->csi.params[1] - 1;
result.mouse.position.y = vt_parser_state->csi.params[2] - 1;
goto done;
}
break;
case 't':
switch (vt_parser_state->csi.params[0]) {
case 8: // Window Size
result.type = UI_INPUT_RESIZE;
result.resize.width = max(vt_parser_state->csi.params[2], 1);
result.resize.height = max(vt_parser_state->csi.params[1], 1);
assert(result.resize.width > 0 && result.resize.height > 0);
assert(result.resize.width < 32768 && result.resize.height < 32768);
goto done;
default:
break;
}
break;
default:
assert(false);
break;
}
break;
default:
break;
}
}
parse_modifiers:
i32 p1 = max(vt_parser_state->csi.params[1] - 1, 0);
if (p1 & 0x01) {
result.keyboard.modifiers |= KEYBOARD_MODIFIER_SHIFT;
}
if (p1 & 0x02) {
result.keyboard.modifiers |= KEYBOARD_MODIFIER_ALT;
}
if (p1 & 0x04) {
result.keyboard.modifiers |= KEYBOARD_MODIFIER_CTRL;
}
done:
*input = s8_slice(*input, it - input->beg, USIZE_MAX);
return result;
}

9
src/input.h Normal file
View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "tui.h"
#include "vt.h"
UiInput get_next_ui_input(VtParserState* vt_parser_state, s8* input);

372
src/loc.c Normal file
View File

@ -0,0 +1,372 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "loc.h"
#include "arena.h"
#define L(s) {(c8*)s, sizeof(s) - 1}
// TODO: Move this to the OS/platform layer
#define MUI_LANGUAGE_NAME 0x8 // Use ISO language (culture) name convention
__declspec(dllimport) i32 __stdcall GetUserPreferredUILanguages(u32 dwFlags, u32* pulNumLanguages, c16* pwszLanguagesBuffer, u32* pcchLanguagesBuffer);
typedef enum LangId {
// Base language. It's always the first one.
LANG_en,
// Other languages. Sorted alphabetically.
LANG_de,
LANG_es,
LANG_fr,
LANG_it,
LANG_ja,
LANG_ko,
LANG_pt_br,
LANG_ru,
LANG_zh_hans,
LANG_zh_hant,
LANG_COUNT,
} LangId;
static const s8 s_lang_lut[LOC_COUNT][LANG_COUNT] = {
[LOC_Ctrl] = {
[LANG_en] = L("Ctrl"),
[LANG_de] = L("Strg"),
[LANG_es] = L("Ctrl"),
[LANG_fr] = L("Ctrl"),
[LANG_it] = L("Ctrl"),
[LANG_ja] = L("Ctrl"),
[LANG_ko] = L("Ctrl"),
[LANG_pt_br] = L("Ctrl"),
[LANG_ru] = L("Ctrl"),
[LANG_zh_hans] = L("Ctrl"),
[LANG_zh_hant] = L("Ctrl"),
},
[LOC_Alt] = {
[LANG_en] = L("Alt"),
[LANG_de] = L("Alt"),
[LANG_es] = L("Alt"),
[LANG_fr] = L("Alt"),
[LANG_it] = L("Alt"),
[LANG_ja] = L("Alt"),
[LANG_ko] = L("Alt"),
[LANG_pt_br] = L("Alt"),
[LANG_ru] = L("Alt"),
[LANG_zh_hans] = L("Alt"),
[LANG_zh_hant] = L("Alt"),
},
[LOC_Shift] = {
[LANG_en] = L("Shift"),
[LANG_de] = L("Umschalt"),
[LANG_es] = L("Mayús"),
[LANG_fr] = L("Maj"),
[LANG_it] = L("Maiusc"),
[LANG_ja] = L("Shift"),
[LANG_ko] = L("Shift"),
[LANG_pt_br] = L("Shift"),
[LANG_ru] = L("Shift"),
[LANG_zh_hans] = L("Shift"),
[LANG_zh_hant] = L("Shift"),
},
[LOC_File] = {
[LANG_en] = L("File"),
[LANG_de] = L("Datei"),
[LANG_es] = L("Archivo"),
[LANG_fr] = L("Fichier"),
[LANG_it] = L("File"),
[LANG_ja] = L("ファイル"),
[LANG_ko] = L("파일"),
[LANG_pt_br] = L("Arquivo"),
[LANG_ru] = L("Файл"),
[LANG_zh_hans] = L("文件"),
[LANG_zh_hant] = L("檔案"),
},
[LOC_File_Save] = {
[LANG_en] = L("Save"),
[LANG_de] = L("Speichern"),
[LANG_es] = L("Guardar"),
[LANG_fr] = L("Enregistrer"),
[LANG_it] = L("Salva"),
[LANG_ja] = L("保存"),
[LANG_ko] = L("저장"),
[LANG_pt_br] = L("Salvar"),
[LANG_ru] = L("Сохранить"),
[LANG_zh_hans] = L("保存"),
[LANG_zh_hant] = L("儲存"),
},
[LOC_File_Save_As] = {
[LANG_en] = L("Save As"),
[LANG_de] = L("Speichern unter"),
[LANG_es] = L("Guardar Como"),
[LANG_fr] = L("Enregistrer sous"),
[LANG_it] = L("Salva come"),
[LANG_ja] = L("名前を付けて保存"),
[LANG_ko] = L("다른 이름으로 저장"),
[LANG_pt_br] = L("Salvar Como"),
[LANG_ru] = L("Сохранить как"),
[LANG_zh_hans] = L("另存为"),
[LANG_zh_hant] = L("另存新檔"),
},
[LOC_File_Exit] = {
[LANG_en] = L("Exit"),
[LANG_de] = L("Beenden"),
[LANG_es] = L("Salir"),
[LANG_fr] = L("Quitter"),
[LANG_it] = L("Esci"),
[LANG_ja] = L("終了"),
[LANG_ko] = L("종료"),
[LANG_pt_br] = L("Sair"),
[LANG_ru] = L("Выход"),
[LANG_zh_hans] = L("退出"),
[LANG_zh_hant] = L("退出"),
},
[LOC_Edit] = {
[LANG_en] = L("Edit"),
[LANG_de] = L("Bearbeiten"),
[LANG_es] = L("Editar"),
[LANG_fr] = L("Éditer"),
[LANG_it] = L("Modifica"),
[LANG_ja] = L("編集"),
[LANG_ko] = L("편집"),
[LANG_pt_br] = L("Editar"),
[LANG_ru] = L("Правка"),
[LANG_zh_hans] = L("编辑"),
[LANG_zh_hant] = L("編輯"),
},
[LOC_Edit_Undo] = {
[LANG_en] = L("Undo"),
[LANG_de] = L("Rückgängig"),
[LANG_es] = L("Deshacer"),
[LANG_fr] = L("Annuler"),
[LANG_it] = L("Annulla"),
[LANG_ja] = L("元に戻す"),
[LANG_ko] = L("실행 취소"),
[LANG_pt_br] = L("Desfazer"),
[LANG_ru] = L("Отменить"),
[LANG_zh_hans] = L("撤销"),
[LANG_zh_hant] = L("復原"),
},
[LOC_Edit_Redo] = {
[LANG_en] = L("Redo"),
[LANG_de] = L("Wiederholen"),
[LANG_es] = L("Rehacer"),
[LANG_fr] = L("Rétablir"),
[LANG_it] = L("Ripeti"),
[LANG_ja] = L("やり直し"),
[LANG_ko] = L("다시 실행"),
[LANG_pt_br] = L("Refazer"),
[LANG_ru] = L("Повторить"),
[LANG_zh_hans] = L("重做"),
[LANG_zh_hant] = L("重做"),
},
[LOC_Edit_Cut] = {
[LANG_en] = L("Cut"),
[LANG_de] = L("Ausschneiden"),
[LANG_es] = L("Cortar"),
[LANG_fr] = L("Couper"),
[LANG_it] = L("Taglia"),
[LANG_ja] = L("切り取り"),
[LANG_ko] = L("잘라내기"),
[LANG_pt_br] = L("Cortar"),
[LANG_ru] = L("Вырезать"),
[LANG_zh_hans] = L("剪切"),
[LANG_zh_hant] = L("剪下"),
},
[LOC_Edit_Copy] = {
[LANG_en] = L("Copy"),
[LANG_de] = L("Kopieren"),
[LANG_es] = L("Copiar"),
[LANG_fr] = L("Copier"),
[LANG_it] = L("Copia"),
[LANG_ja] = L("コピー"),
[LANG_ko] = L("복사"),
[LANG_pt_br] = L("Copiar"),
[LANG_ru] = L("Копировать"),
[LANG_zh_hans] = L("复制"),
[LANG_zh_hant] = L("複製"),
},
[LOC_Edit_Paste] = {
[LANG_en] = L("Paste"),
[LANG_de] = L("Einfügen"),
[LANG_es] = L("Pegar"),
[LANG_fr] = L("Coller"),
[LANG_it] = L("Incolla"),
[LANG_ja] = L("貼り付け"),
[LANG_ko] = L("붙여넣기"),
[LANG_pt_br] = L("Colar"),
[LANG_ru] = L("Вставить"),
[LANG_zh_hans] = L("粘贴"),
[LANG_zh_hant] = L("貼上"),
},
[LOC_Edit_Find] = {
[LANG_en] = L("Find"),
[LANG_de] = L("Suchen"),
[LANG_es] = L("Buscar"),
[LANG_fr] = L("Rechercher"),
[LANG_it] = L("Trova"),
[LANG_ja] = L("検索"),
[LANG_ko] = L("찾기"),
[LANG_pt_br] = L("Encontrar"),
[LANG_ru] = L("Найти"),
[LANG_zh_hans] = L("查找"),
[LANG_zh_hant] = L("尋找"),
},
[LOC_Edit_Replace] = {
[LANG_en] = L("Replace"),
[LANG_de] = L("Ersetzen"),
[LANG_es] = L("Reemplazar"),
[LANG_fr] = L("Remplacer"),
[LANG_it] = L("Sostituisci"),
[LANG_ja] = L("置換"),
[LANG_ko] = L("바꾸기"),
[LANG_pt_br] = L("Substituir"),
[LANG_ru] = L("Заменить"),
[LANG_zh_hans] = L("替换"),
[LANG_zh_hant] = L("取代"),
},
[LOC_Help] = {
[LANG_en] = L("Help"),
[LANG_de] = L("Hilfe"),
[LANG_es] = L("Ayuda"),
[LANG_fr] = L("Aide"),
[LANG_it] = L("Aiuto"),
[LANG_ja] = L("ヘルプ"),
[LANG_ko] = L("도움말"),
[LANG_pt_br] = L("Ajuda"),
[LANG_ru] = L("Помощь"),
[LANG_zh_hans] = L("帮助"),
[LANG_zh_hant] = L("幫助"),
},
[LOC_Help_About] = {
[LANG_en] = L("About"),
[LANG_de] = L("Über"),
[LANG_es] = L("Acerca de"),
[LANG_fr] = L("À propos"),
[LANG_it] = L("Informazioni"),
[LANG_ja] = L("情報"),
[LANG_ko] = L("정보"),
[LANG_pt_br] = L("Sobre"),
[LANG_ru] = L("О программе"),
[LANG_zh_hans] = L("关于"),
[LANG_zh_hant] = L("關於"),
},
[LOC_Exit_Dialog_Title] = {
[LANG_en] = L("Exit without saving?"),
[LANG_de] = L("Ohne Speichern beenden?"),
[LANG_es] = L("¿Salir sin guardar?"),
[LANG_fr] = L("Quitter sans enregistrer ?"),
[LANG_it] = L("Uscire senza salvare?"),
[LANG_ja] = L("保存せずに終了しますか?"),
[LANG_ko] = L("저장하지 않고 종료하시겠습니까?"),
[LANG_pt_br] = L("Sair sem salvar?"),
[LANG_ru] = L("Выйти без сохранения?"),
[LANG_zh_hans] = L("退出前是否保存?"),
[LANG_zh_hant] = L("退出不儲存?"),
},
[LOC_Exit_Dialog_Yes] = {
[LANG_en] = L("Yes"),
[LANG_de] = L("Ja"),
[LANG_es] = L(""),
[LANG_fr] = L("Oui"),
[LANG_it] = L(""),
[LANG_ja] = L("はい"),
[LANG_ko] = L(""),
[LANG_pt_br] = L("Sim"),
[LANG_ru] = L("Да"),
[LANG_zh_hans] = L(""),
[LANG_zh_hant] = L(""),
},
[LOC_Exit_Dialog_No] = {
[LANG_en] = L("No"),
[LANG_de] = L("Nein"),
[LANG_es] = L("No"),
[LANG_fr] = L("Non"),
[LANG_it] = L("No"),
[LANG_ja] = L("いいえ"),
[LANG_ko] = L("아니요"),
[LANG_pt_br] = L("Não"),
[LANG_ru] = L("Нет"),
[LANG_zh_hans] = L(""),
[LANG_zh_hant] = L(""),
},
};
static int s_lang;
void loc_init()
{
// Get the user's preferred UI language
u32 lang_num = 0;
c16 lang_buf[256] = {0};
u32 lang_buf_len = array_size(lang_buf);
if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &lang_num, lang_buf, &lang_buf_len) || lang_num == 0) {
return;
}
// transform to lowercase
for (u32 i = 0; i < lang_buf_len; ++i) {
if (lang_buf[i] >= 'A' && lang_buf[i] <= 'Z') {
lang_buf[i] += 'a' - 'A';
}
}
// TODO: Iterate through the null-delimited list of language names using wcslen() and convert each to UTF-8. Don't use lang_num
c16* beg = &lang_buf[0];
for (; *beg; beg += wcslen(beg)) {
if (wcsncmp(beg, L"en", 2) == 0) {
s_lang = LANG_en;
break;
}
if (wcsncmp(beg, L"de", 2) == 0) {
s_lang = LANG_de;
break;
}
if (wcsncmp(beg, L"es", 2) == 0) {
s_lang = LANG_es;
break;
}
if (wcsncmp(beg, L"fr", 2) == 0) {
s_lang = LANG_fr;
break;
}
if (wcsncmp(beg, L"it", 2) == 0) {
s_lang = LANG_it;
break;
}
if (wcsncmp(beg, L"ja", 2) == 0) {
s_lang = LANG_ja;
break;
}
if (wcsncmp(beg, L"ko", 2) == 0) {
s_lang = LANG_ko;
break;
}
if (wcsncmp(beg, L"pt-br", 5) == 0) {
s_lang = LANG_pt_br;
break;
}
if (wcsncmp(beg, L"ru", 2) == 0) {
s_lang = LANG_ru;
break;
}
if (wcsncmp(beg, L"zh-hant", 7) == 0) {
s_lang = LANG_zh_hant;
break;
}
if (wcsncmp(beg, L"zh", 2) == 0) {
s_lang = LANG_zh_hans;
break;
}
}
}
s8 loc(LocId id)
{
return s_lang_lut[id][s_lang];
}

43
src/loc.h Normal file
View File

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "helpers.h"
typedef enum LocId
{
LOC_Ctrl,
LOC_Alt,
LOC_Shift,
// File menu
LOC_File,
LOC_File_Save,
LOC_File_Save_As,
LOC_File_Exit,
// Edit menu
LOC_Edit,
LOC_Edit_Undo,
LOC_Edit_Redo,
LOC_Edit_Cut,
LOC_Edit_Copy,
LOC_Edit_Paste,
LOC_Edit_Find,
LOC_Edit_Replace,
// Help menu
LOC_Help,
LOC_Help_About,
// Exit dialog
LOC_Exit_Dialog_Title,
LOC_Exit_Dialog_Yes,
LOC_Exit_Dialog_No,
LOC_COUNT,
} LocId;
void loc_init();
s8 loc(LocId id);

570
src/main.c Normal file
View File

@ -0,0 +1,570 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "arena.h"
#include "buffer.h"
#include "input.h"
#include "loc.h"
#include "os.h"
#include "ucd.h"
// TODO:
// * Cut/Copy/Paste
// * Find with ICU
// * Line wrapping
// * Verifying UTF8 on load
// * Load non-UTF8 files
// * Focus into submenus
// * How to know that the parent menu is still open?
// * Dialog for Save-As
// * Perhaps as an input line in the status bar
// * Accelerators (文件 (F))
// * Undo/Redo batching/grouping
// * Output diffing / compression
// * OOM/IO error handling
// --------------------------------------------------
// * Replace
// * Multi-Cursor
// * Scrolling by dragging the track/thumb
// * When selecting text, hug the line contents like in VS Code
int main(int argc, c8** argv)
{
os_init();
if (argc != 2) {
os_write_stdout(S("Usage: edit <file>\r\n"));
os_deinit();
return 1;
}
loc_init();
VtParserState vt_parser_state = {};
UiContext* ctx = ui_root_create();
bool wants_save = false;
bool wants_exit = false;
{
ScratchArena scratch = scratch_beg(NULL);
s8 request = {};
for (u32 i = 0; i < 16; ++i) {
s8_append_fmt(scratch.arena, &request, "\033]4;", i, ";?\033\\");
}
s8_append(scratch.arena, &request, S("\033[c"));
os_write_stdout(request);
u32 indexed_colors[16] = {
0xff000000,
0xff212cbe,
0xff3aae3f,
0xff4a9abe,
0xffbe4d20,
0xffbe54bb,
0xffb2a700,
0xffbebebe,
0xff808080,
0xff303eff,
0xff51ea58,
0xff44c9ff,
0xffff6a2f,
0xffff74fc,
0xfff0e100,
0xffffffff,
};
for (;;) {
s8 response = os_read_stdin(scratch.arena);
if (response.len == 0) {
goto exit;
}
c8* it = response.beg;
c8* end = response.beg + response.len;
do {
// TODO: Need to fix vt_parse_next_token to support broken up OSC sequences and distinguishing between a completed and uncompleted ones.
it = vt_parse_next_token(&vt_parser_state, it, end);
if (vt_parser_state.kind == VT_TOKEN_KIND_OSC) {
s8 osc = vt_parser_state.osc;
// The response is in the form of `4;<color>;rgb:<r>/<g>/<b>`.
if (s8_starts_with(osc, S("4;"))) {
usize off = 2;
// Parse the <color>
u32 color = 0;
for (; off < osc.len && osc.beg[off] != ';'; ++off) {
color = color * 10 + (osc.beg[off] - '0');
}
if (color >= 16) {
continue;
}
osc = s8_slice(osc, off + 1, osc.len);
if (!s8_starts_with(osc, S("rgb:"))) {
continue;
}
u32 rgb = 0;
off = 4;
for (int j = 0; j < 3; j++) {
usize sep = s8_find(osc, off, '/');
usize len = sep - off;
if (len == 2 || len == 4) {
u64 val = s8_to_u64(s8_slice(osc, off, sep), 16);
if (len == 4) {
val = (val * 0xff + 0x80) / 0xffff;
}
rgb = (rgb >> 8) | (u32)(val << 16);
}
off = sep + 1;
}
indexed_colors[color] = rgb | 0xff000000;
}
} else if (vt_parser_state.kind == VT_TOKEN_KIND_CSI) {
if (vt_parser_state.csi.final_byte == 'c') {
goto setup_done;
}
}
} while (it != end);
}
setup_done:
ui_root_setup_indexed_colors(ctx, indexed_colors);
scratch_end(scratch);
}
TextBuffer* tb = text_buffer_create(text_buffer_allocator_default());
text_buffer_read_file(tb, s8_from_ptr(argv[1]));
{
ScratchArena scratch = scratch_beg(NULL);
s8 text = {};
s8_reserve(scratch.arena, &text, tb->text_length);
text.len = text_buffer_extract(tb, 0, tb->text_length, text.beg);
UcdMeasurement wrap;
ucd_measure_forward(text, 0, (Point){}, 20, -1, &wrap);
scratch_end(scratch);
}
// 1049: Alternative Screen Buffer
// I put the ASB switch in the beginning, just in case the terminal performs
// some additional state tracking beyond the modes we enable/disable.
// 1002: Cell Motion Mouse Tracking
// 1006: SGR Mouse Mode
// 2004: Bracketed Paste Mode
os_write_stdout(S("\x1b[?1049h\x1b[?1002;1006;2004h"));
os_inject_window_size_into_stdin();
while (true) {
ScratchArena scratch = scratch_beg(NULL);
s8 input = os_read_stdin(scratch.arena);
if (input.len == 0) {
scratch_end(scratch);
break;
}
for (;;) {
bool last_pass = input.len == 0;
UiInput ui_input = get_next_ui_input(&vt_parser_state, &input);
// 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 (ui_input.type == UI_INPUT_RESIZE && memcmp(&ui_input.resize, &ctx->size, sizeof(Size)) == 0) {
continue;
}
ctx = ui_root_reset(ctx, ui_input);
ui_menubar_begin(ctx);
ui_attr_background_rgba(ctx, 0x3f7f7f7f);
ui_attr_foreground_rgba(ctx, 0xffffffff);
{
if (ui_menubar_menu_begin(ctx, loc(LOC_File), 'F')) {
if (ui_menubar_menu_item(ctx, loc(LOC_File_Save), 'S', KEYBOARD_MODIFIER_CTRL | 'S')) {
wants_save = true;
}
if (ui_menubar_menu_item(ctx, loc(LOC_File_Save_As), 'A', KEYBOARD_MODIFIER_CTRL | KEYBOARD_MODIFIER_SHIFT | 'S')) {
}
if (ui_menubar_menu_item(ctx, loc(LOC_File_Exit), 'X', KEYBOARD_MODIFIER_CTRL | 'Q')) {
wants_exit = true;
}
ui_menubar_menu_end(ctx);
}
if (ui_menubar_menu_begin(ctx, loc(LOC_Edit), 'E')) {
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Undo), 'U', KEYBOARD_MODIFIER_CTRL | 'Z')) {
// TODO: This and the ones below are essentially hacks. I'm not 100% sure how to inject the input yet.
// I could use bools and call the corresponding functions on the text buffer below, perhaps?
ctx->input_mouse_action = MOUSE_ACTION_NONE;
ctx->input_keyboard.key = VK_Z;
ctx->input_keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
ctx->input_consumed = false;
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Redo), 'R', KEYBOARD_MODIFIER_CTRL | 'Y')) {
ctx->input_mouse_action = MOUSE_ACTION_NONE;
ctx->input_keyboard.key = VK_Y;
ctx->input_keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
ctx->input_consumed = false;
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Cut), 'T', KEYBOARD_MODIFIER_CTRL | 'X')) {
ctx->input_mouse_action = MOUSE_ACTION_NONE;
ctx->input_keyboard.key = VK_X;
ctx->input_keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
ctx->input_consumed = false;
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Copy), 'C', KEYBOARD_MODIFIER_CTRL | 'C')) {
ctx->input_mouse_action = MOUSE_ACTION_NONE;
ctx->input_keyboard.key = VK_C;
ctx->input_keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
ctx->input_consumed = false;
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Paste), 'P', KEYBOARD_MODIFIER_CTRL | 'V')) {
ctx->input_mouse_action = MOUSE_ACTION_NONE;
ctx->input_keyboard.key = VK_V;
ctx->input_keyboard.modifiers = KEYBOARD_MODIFIER_CTRL;
ctx->input_consumed = false;
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Find), 'F', KEYBOARD_MODIFIER_CTRL | 'F')) {
}
if (ui_menubar_menu_item(ctx, loc(LOC_Edit_Replace), 'R', KEYBOARD_MODIFIER_CTRL | 'H')) {
}
ui_menubar_menu_end(ctx);
}
if (ui_menubar_menu_begin(ctx, loc(LOC_Help), 'H')) {
if (ui_menubar_menu_item(ctx, loc(LOC_Help_About), 'A', 0)) {
}
ui_menubar_menu_end(ctx);
}
}
ui_menubar_end(ctx);
ui_focus_next_by_default(ctx);
ui_textarea(ctx, tb, (Size){0, ui_root_get_size(ctx).height - 2});
ui_container_begin_named(ctx, S("statusbar"));
ui_attr_background_rgba(ctx, 0x3f7f7f7f);
ui_attr_foreground_rgba(ctx, 0xffffffff);
{
s8 status = {};
s8_append_fmt(scratch.arena, &status, "Ln ", tb->cursor.logical_pos.y + 1, ", Col ", tb->cursor.logical_pos.x + 1);
s8_append(scratch.arena, &status, tb->overtype ? S(" OVR") : S(" INS"));
ui_label(ctx, status);
ui_attr_padding(ctx, (Rect){1, 0, 1, 0});
}
ui_container_end(ctx);
if (wants_save) {
text_buffer_write_file(tb, s8_from_ptr(argv[1]));
wants_save = false;
}
if (wants_exit) {
if (!tb->dirty) {
goto exit;
}
ui_container_begin_named(ctx, S("exit"));
ui_attr_foreground_indexed(ctx, 15);
ui_attr_background_indexed(ctx, 1);
ui_attr_border(ctx);
ui_attr_float(ctx, (UiFloatSpec){.gravity_x = 0.5f, .gravity_y = 0.5f, .offset_x = ctx->size.width / 2, .offset_y = ctx->size.height / 2});
{
ui_label(ctx, loc(LOC_Exit_Dialog_Title));
ui_attr_padding(ctx, (Rect){2, 0, 2, 1});
ui_container_begin_named(ctx, S("buttons"));
ui_attr_grid_columns(ctx, (UiGridColumns){.count = 2, .widths = (CoordType[]){-1, -1}});
{
if (ui_button(ctx, loc(LOC_Exit_Dialog_Yes))) {
goto exit;
}
if (ui_button(ctx, loc(LOC_Exit_Dialog_No))) {
wants_exit = false;
}
}
ui_container_end(ctx);
}
ui_container_end(ctx);
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'S')) {
wants_save = true;
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | KEYBOARD_MODIFIER_SHIFT | 'S')) {
debug_print("Save As\n");
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'Q')) {
wants_exit = true;
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'X')) {
debug_print("Cut\n");
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'C')) {
debug_print("Copy\n");
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'V')) {
debug_print("Paste\n");
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'F')) {
debug_print("Find\n");
}
if (ui_consume_shortcut(ctx, KEYBOARD_MODIFIER_CTRL | 'H')) {
debug_print("Replace\n");
}
if (last_pass) {
break;
}
}
s8 output = ui_root_render(ctx);
os_write_stdout(output);
scratch_end(scratch);
}
exit:
// Same as in the beginning but in the reverse order.
// It also includes DECSCUSR 0 to reset the cursor style.
os_write_stdout(S("\x1b[?1002;1006;2004l\x1b[?1049l\x1b[0 q"));
os_deinit();
return 0;
}
#ifdef NODEFAULTLIB
extern int __cdecl __isa_available_init();
__declspec(dllimport) c8* __stdcall GetCommandLineA();
__declspec(dllimport) u32 GetModuleFileNameA(void* hModule, c8* lpFilename, u32 nSize);
__declspec(dllimport) void __stdcall ExitProcess(u32 uExitCode);
static bool Parse_Cmdline(const c8* cmdstart, c8** argv, c8* lpstr, int* numargs, int* numbytes)
{
const c8* p;
c8 c;
int inquote; /* 1 = inside quotes */
int copychar; /* 1 = copy char to *args */
short numslash; /* num of backslashes seen */
int nbytes = 0;
int argc = *numargs; /* store the value to check for buffer overrun */
*numargs = 1; /* the program name at least */
/* first scan the program name, copy it, and count the bytes */
p = cmdstart;
if (argv) {
*argv++ = lpstr;
__analysis_assume(lpstr != NULL);
} else {
__analysis_assume(lpstr == NULL);
}
/* A quoted program name is handled here. The handling is much
simpler than for other arguments. Basically, whatever lies
between the leading double-quote and next one, or a terminal null
character is simply accepted. Fancier handling is not required
because the program name must be a legal NTFS/HPFS file name.
Note that the double-quote characters are not copied, nor do they
contribute to numbytes. */
if (*p == '"') {
/* scan from just past the first double-quote through the next
double-quote, or up to a null, whichever comes first */
while (*++p != '"' && *p != '\0') {
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = *p;
}
}
}
/* append the terminating null */
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = '\0';
}
}
/* if we stopped on a double-quote (usual case), skip over it */
if (*p == '"')
p++;
} else {
/* Not a quoted program name */
do {
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = *p;
}
}
c = (c8)*p++;
} while (c > ' ');
if (c == '\0') {
p--;
} else {
if (lpstr) {
if (nbytes <= *numbytes) {
*(lpstr - 1) = '\0';
}
}
}
}
inquote = 0;
/* loop on each argument */
for (;;) {
if (*p) {
while (*p == ' ' || *p == '\t')
++p;
}
if (*p == '\0')
break; /* end of args */
/* scan an argument */
if (argv && *numargs < argc) {
*argv++ = lpstr; /* store ptr to arg */
}
++*numargs; /* found another arg */
/* loop through scanning one argument */
for (;;) {
copychar = 1;
/* Rules: 2N backslashes + " ==> N backslashes and begin/end quote
2N+1 backslashes + " ==> N backslashes + literal "
N backslashes ==> N backslashes */
numslash = 0;
while (*p == '\\') {
/* count number of backslashes for use below */
++p;
++numslash;
}
if (*p == '"') {
/* if 2N backslashes before, start/end quote, otherwise
copy literally */
if (numslash % 2 == 0) {
if (inquote)
if (p[1] == '"')
p++; /* Double quote inside quoted string */
else /* skip first quote char and copy second */
copychar = 0;
else
copychar = 0; /* don't copy quote */
inquote = !inquote;
}
numslash /= 2; /* divide numslash by two */
}
/* copy slashes */
while (numslash--) {
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = '\\';
}
}
}
/* if at end of arg, break loop */
if (*p == '\0' || (!inquote && (*p == ' ' || *p == '\t')))
break;
/* copy character into argument */
if (copychar) {
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = *p;
}
}
}
++p;
}
/* null-terminate the argument */
nbytes += sizeof(c8);
if (lpstr) {
if (nbytes <= *numbytes) {
*lpstr++ = '\0'; /* terminate string */
}
}
}
if (lpstr) {
return nbytes <= *numbytes && *numargs <= argc;
} else {
*numbytes = nbytes;
return true;
}
}
static c8** CommandLineToArgvA(Arena* arena, const c8* lpCmdLine, int* pNumArgs)
{
c8** argv_U = NULL;
const c8* cmdstart; /* start of command line to parse */
int numbytes = 0;
c8 pgmname[260];
if (NULL != pNumArgs) {
/* Get the program name pointer from Win32 Base */
GetModuleFileNameA(NULL, pgmname, sizeof(pgmname) / sizeof(c8));
/* if there's no command line at all (won't happen from cmd.exe, but
possibly another program), then we use pgmname as the command line
to parse, so that argv[0] is initialized to the program name */
cmdstart = *lpCmdLine == '\0' ? pgmname : lpCmdLine;
/* set *pNumArgs to 1 to start with */
*pNumArgs = 1;
/* first find out how much space is needed to store args */
Parse_Cmdline(cmdstart, NULL, NULL, pNumArgs, &numbytes);
// 0 < ((*pNumArgs+1) * sizeof(c8*) + numbytes) <= INT_MAX
if (0 < *pNumArgs && *pNumArgs <= (int)((INT_MAX - numbytes) / sizeof(c8*) - 1)) {
/* allocate space for argv[] vector and strings */
argv_U = (c8**)arena_zalloc_raw(arena, (*pNumArgs + 1) * sizeof(c8*) + numbytes, 16);
if (argv_U) {
/* store args and argv ptrs in just allocated block */
bool bVal = Parse_Cmdline(cmdstart, argv_U, (c8*)argv_U + (*pNumArgs + 1) * sizeof(c8*), pNumArgs, &numbytes);
if (!bVal) {
argv_U = NULL;
}
}
}
}
return argv_U;
}
extern void mainCRTStartup()
{
// __isa_available_init ensures that `extern int __isa_available` has the right value.
// The msvcrt/vcruntime technically initializes a bunch of other things, but we need none of those.
__isa_available_init();
ScratchArena scratch = scratch_beg(NULL);
c8* command_line = GetCommandLineA();
int argc = 0;
c8** argv = CommandLineToArgvA(scratch.arena, command_line, &argc);
ExitProcess(main(argc, argv));
}
#endif

78
src/os.h Normal file
View File

@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "arena.h"
typedef u32 PlatformError;
void os_init();
void os_deinit();
void os_inject_window_size_into_stdin();
s8 os_read_stdin(Arena* arena);
void os_write_stdout(s8 str);
void* os_virtual_reserve(usize size);
void os_virtual_release(void* base);
void os_virtual_commit(void* base, usize size);
void* os_open_file_for_reading(s8 path);
void* os_open_file_for_writing(s8 path);
usize os_file_size(void* handle);
void os_close_file(void* handle);
usize os_read_file(void* handle, void* buffer, usize length);
PlatformError os_write_file(void* handle, void* buffer, usize length);
#ifdef _MSC_VER
// C4047: '=': '...' differs in levels of indirection from 'os_anon_proc'
// C4113: 'os_anon_proc' differs in parameter lists from '...'
// C4113: '=': incompatible types - from 'os_anon_proc' to '...'
#define OS_SUPPRESS_GET_PROC_NAGGING_BEGIN __pragma(warning(push)) __pragma(warning(disable : 4047 4113 4133))
#define OS_SUPPRESS_GET_PROC_NAGGING_END __pragma(warning(pop))
#else
#define OS_SUPPRESS_GET_PROC_NAGGING_BEGIN
#define OS_SUPPRESS_GET_PROC_NAGGING_END
#endif
typedef void (*os_anon_proc)();
void* os_load_library(const char* name);
os_anon_proc os_get_proc_address(void* library, const char* name);
inline u32 os_bit_width_u32(u32 val)
{
unsigned long index;
if (!_BitScanReverse(&index, val)) {
index = (unsigned long)-1;
}
return index + 1;
}
inline u32 os_bit_width_u64(u64 val)
{
unsigned long index;
if (!_BitScanReverse64(&index, val)) {
index = (unsigned long)-1;
}
return index + 1;
}
#define os_bit_width(val) _Generic((val), u32: os_bit_width_u32, u64: os_bit_width_u64)(val)
inline u32 os_bit_ceil_u32(u32 val)
{
return (u32)1 << os_bit_width_u32(val - 1);
}
inline u64 os_bit_ceil_u64(u64 val)
{
return (u64)1 << os_bit_width_u64(val - 1);
}
#define os_bit_ceil(val) _Generic((val), u32: os_bit_ceil_u32, u64: os_bit_ceil_u64)(val)
#ifndef NDEBUG
void debug_print(const char* fmt, ...);
#else
#define debug_print(...) ((void)0)
#endif

287
src/os_win32.c Normal file
View File

@ -0,0 +1,287 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "os.h"
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#define CONSOLE_READ_NOWAIT 0x0002
typedef BOOL(WINAPI* fn_ReadConsoleInputExW)(HANDLE hConsoleInput, PINPUT_RECORD lpBuffer, DWORD nLength, LPDWORD lpNumberOfEventsRead, USHORT wFlags);
static fn_ReadConsoleInputExW s_ReadConsoleInputExW;
static HANDLE s_stdin;
static HANDLE s_stdout;
static DWORD s_stdin_cp_old;
static DWORD s_stdout_cp_old;
static DWORD s_stdin_mode_old;
static DWORD s_stdout_mode_old;
static bool s_inject_resize;
static bool s_wants_exit;
// UTF-16 text
typedef struct s16 {
c16* beg;
usize len;
usize cap;
} s16;
static s8 s16_to_s8(Arena* arena, s16 str)
{
s8 result = {};
result.len = WideCharToMultiByte(CP_UTF8, 0, str.beg, (int)str.len, NULL, 0, NULL, NULL);
result.beg = arena_mallocn(arena, c8, result.len);
WideCharToMultiByte(CP_UTF8, 0, str.beg, (int)str.len, (LPSTR)result.beg, (int)result.len, NULL, NULL);
return result;
}
static LPWSTR s8_to_lpwstr(Arena* arena, s8 str)
{
int len = MultiByteToWideChar(CP_UTF8, 0, (LPSTR)str.beg, (int)str.len, NULL, 0);
if (len <= 0) {
return NULL;
}
LPWSTR beg = arena_mallocn(arena, wchar_t, len + 1);
len = MultiByteToWideChar(CP_UTF8, 0, (LPSTR)str.beg, (int)str.len, (LPWSTR)beg, len);
if (len <= 0) {
return NULL;
}
beg[len] = 0;
return beg;
}
static BOOL WINAPI console_ctrl_handler(DWORD ctrl_type)
{
s_wants_exit = true;
CancelIoEx(s_stdin, NULL);
return true;
}
void os_init()
{
s_ReadConsoleInputExW = (fn_ReadConsoleInputExW)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "ReadConsoleInputExW");
SetConsoleCtrlHandler(console_ctrl_handler, true);
s_stdin = GetStdHandle(STD_INPUT_HANDLE);
s_stdout = GetStdHandle(STD_OUTPUT_HANDLE);
s_stdin_cp_old = GetConsoleCP();
s_stdout_cp_old = GetConsoleOutputCP();
GetConsoleMode(s_stdin, &s_stdin_mode_old);
GetConsoleMode(s_stdout, &s_stdout_mode_old);
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
SetConsoleMode(s_stdin, ENABLE_WINDOW_INPUT | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT);
SetConsoleMode(s_stdout, ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
}
void os_deinit()
{
SetConsoleCP(s_stdin_cp_old);
SetConsoleOutputCP(s_stdout_cp_old);
SetConsoleMode(s_stdin, s_stdin_mode_old);
SetConsoleMode(s_stdout, s_stdout_mode_old);
}
void os_inject_window_size_into_stdin()
{
s_inject_resize = true;
}
s8 os_read_stdin(Arena* arena)
{
s8 text = {};
if (s_inject_resize) {
s_inject_resize = false;
CONSOLE_SCREEN_BUFFER_INFOEX info = {};
info.cbSize = sizeof(info);
if (!GetConsoleScreenBufferInfoEx(s_stdout, &info)) {
return (s8){};
}
int w = max(info.srWindow.Right - info.srWindow.Left + 1, 1);
int h = max(info.srWindow.Bottom - info.srWindow.Top + 1, 1);
s8_append_fmt(arena, &text, "\x1b[8;", h, ";", w, "t");
}
while (true) {
INPUT_RECORD input[1024];
DWORD read = 0;
USHORT flags = text.len == 0 ? 0 : CONSOLE_READ_NOWAIT;
if (!s_ReadConsoleInputExW(s_stdin, &input[0], array_size(input), &read, flags) || s_wants_exit) {
return (s8){};
}
for (DWORD i = 0; i < read; ++i) {
switch (input[i].EventType) {
case KEY_EVENT: {
const KEY_EVENT_RECORD* event = &input[i].Event.KeyEvent;
if (event->bKeyDown && event->uChar.UnicodeChar) {
// Convert the UCS2 character to UTF8 without calling any functions.
c8 utf8[4];
usize utf8_len = 0;
if (event->uChar.UnicodeChar < 0x80) {
utf8[utf8_len++] = (c8)event->uChar.UnicodeChar;
} else if (event->uChar.UnicodeChar < 0x800) {
utf8[utf8_len++] = (c8)(0xC0 | (event->uChar.UnicodeChar >> 6));
utf8[utf8_len++] = (c8)(0x80 | (event->uChar.UnicodeChar & 0x3F));
} else {
utf8[utf8_len++] = (c8)(0xE0 | (event->uChar.UnicodeChar >> 12));
utf8[utf8_len++] = (c8)(0x80 | ((event->uChar.UnicodeChar >> 6) & 0x3F));
utf8[utf8_len++] = (c8)(0x80 | (event->uChar.UnicodeChar & 0x3F));
}
s8_append(arena, &text, (s8){utf8, utf8_len, utf8_len});
}
break;
}
case WINDOW_BUFFER_SIZE_EVENT: {
const WINDOW_BUFFER_SIZE_RECORD* event = &input[i].Event.WindowBufferSizeEvent;
// 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.
int w = max((int)event->dwSize.X, 1);
int h = max((int)event->dwSize.Y, 1);
s8_append_fmt(arena, &text, "\x1b[8;", h, ";", w, "t");
break;
}
default:
break;
}
}
if (text.len != 0) {
break;
}
}
return text;
}
void os_write_stdout(s8 str)
{
DWORD written = 0;
WriteFile(s_stdout, str.beg, (DWORD)str.len, &written, NULL);
assert(written == str.len);
}
void* os_virtual_reserve(usize size)
{
void* base = NULL;
#ifndef NDEBUG
static uintptr_t s_base_gen;
s_base_gen += 0x0000100000000000;
base = (void*)s_base_gen;
#endif
return VirtualAlloc(base, size, MEM_RESERVE, PAGE_READWRITE);
}
void os_virtual_release(void* base)
{
VirtualFree(base, 0, MEM_RELEASE);
}
void os_virtual_commit(void* base, usize size)
{
VirtualAlloc(base, size, MEM_COMMIT, PAGE_READWRITE);
}
void* os_open_file_for_reading(s8 path)
{
ScratchArena scratch = scratch_beg(NULL);
LPWSTR p = s8_to_lpwstr(scratch.arena, path);
HANDLE h = CreateFileW(p, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
scratch_end(scratch);
return h;
}
void* os_open_file_for_writing(s8 path)
{
ScratchArena scratch = scratch_beg(NULL);
LPWSTR p = s8_to_lpwstr(scratch.arena, path);
HANDLE h = CreateFileW(p, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
scratch_end(scratch);
return h;
}
usize os_file_size(void* handle)
{
LARGE_INTEGER size = {};
GetFileSizeEx(handle, &size);
return (usize)size.QuadPart;
}
void os_close_file(void* handle)
{
CloseHandle(handle);
}
usize os_read_file(void* handle, void* buffer, usize length)
{
DWORD read = 0;
ReadFile(handle, buffer, (DWORD)length, &read, NULL);
return read;
}
static PlatformError os_get_last_error()
{
i32 error = (i32)GetLastError();
if (error <= 0) {
return error;
}
return 0x80070000 | (error & 0x0000ffff);
}
PlatformError os_write_file(void* handle, void* buffer, usize length)
{
u8* bytes = buffer;
while (length != 0) {
DWORD write = (DWORD)min(length, 1024 * 1024 * 1024);
DWORD written = 0;
if (!WriteFile(handle, bytes, write, &written, NULL)) {
return os_get_last_error();
}
bytes += written;
length -= written;
}
return 0;
}
void* os_load_library(const char* name)
{
return LoadLibraryExA(name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
}
os_anon_proc os_get_proc_address(void* library, const char* name)
{
return (os_anon_proc)GetProcAddress(library, name);
}
#ifndef NDEBUG
void debug_print(const char* fmt, ...)
{
char buffer[4096];
va_list args;
va_start(args, fmt);
int len = wvsprintfA(&buffer[0], fmt, args);
va_end(args);
if (len > 0 || len < sizeof(buffer)) {
OutputDebugStringA(&buffer[0]);
}
}
#endif

1637
src/tui.c Normal file

File diff suppressed because it is too large Load Diff

22
src/tui.exe.manifest Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly
xmlns="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
xmlns:cv1="urn:schemas-microsoft-com:compatibility.v1"
xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
xmlns:ws3="http://schemas.microsoft.com/SMI/2019/WindowsSettings"
xmlns:ws4="http://schemas.microsoft.com/SMI/2020/WindowsSettings"
manifestVersion="1.0">
<asmv3:application>
<windowsSettings>
<ws2:longPathAware>true</ws2:longPathAware>
<ws3:activeCodePage>UTF-8</ws3:activeCodePage>
<ws4:heapType>SegmentHeap</ws4:heapType>
</windowsSettings>
</asmv3:application>
<cv1:compatibility>
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</cv1:compatibility>
</assembly>

242
src/tui.h Normal file
View File

@ -0,0 +1,242 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "buffer.h"
typedef struct UiNode UiNode;
typedef struct UiContext UiContext;
typedef struct UiFloatSpec {
// Specifies the origin of the container relative to the container size. [0, 1]
f32 gravity_x;
f32 gravity_y;
// Specifies an offset from the origin in cells.
CoordType offset_x;
CoordType offset_y;
} UiFloatSpec;
typedef struct UiGridColumns {
usize count;
// `data` is a pointer to an array of `CoordType` values.
// Positive values indicate an absolute width in columns.
// Negative values indicate a fraction of the remaining width, similar to the "fr" unit in CSS.
CoordType* widths;
} UiGridColumns;
typedef enum UiInputType {
UI_INPUT_NONE,
UI_INPUT_RESIZE,
UI_INPUT_TEXT,
UI_INPUT_KEYBOARD,
UI_INPUT_MOUSE,
} UiInputType;
typedef enum KeyboardModifier {
KEYBOARD_MODIFIER_NONE = 0x00000000,
KEYBOARD_MODIFIER_CTRL = 0x01000000,
KEYBOARD_MODIFIER_ALT = 0x02000000,
KEYBOARD_MODIFIER_SHIFT = 0x04000000,
} KeyboardModifier;
typedef enum UiInputKeyboardKey {
VK_NULL = 0x00,
VK_BACK = 0x08,
VK_TAB = 0x09,
VK_RETURN = 0x0D,
VK_ESCAPE = 0x1B,
VK_SPACE = 0x20,
VK_PRIOR = 0x21,
VK_NEXT = 0x22,
VK_END = 0x23,
VK_HOME = 0x24,
VK_LEFT = 0x25,
VK_UP = 0x26,
VK_RIGHT = 0x27,
VK_DOWN = 0x28,
VK_INSERT = 0x2D,
VK_DELETE = 0x2E,
VK_A = 'A',
VK_B = 'B',
VK_C = 'C',
VK_D = 'D',
VK_E = 'E',
VK_F = 'F',
VK_G = 'G',
VK_H = 'H',
VK_I = 'I',
VK_J = 'J',
VK_K = 'K',
VK_L = 'L',
VK_M = 'M',
VK_N = 'N',
VK_O = 'O',
VK_P = 'P',
VK_Q = 'Q',
VK_R = 'R',
VK_S = 'S',
VK_T = 'T',
VK_U = 'U',
VK_V = 'V',
VK_W = 'W',
VK_X = 'X',
VK_Y = 'Y',
VK_Z = 'Z',
VK_NUMPAD0 = 0x60,
VK_NUMPAD1 = 0x61,
VK_NUMPAD2 = 0x62,
VK_NUMPAD3 = 0x63,
VK_NUMPAD4 = 0x64,
VK_NUMPAD5 = 0x65,
VK_NUMPAD6 = 0x66,
VK_NUMPAD7 = 0x67,
VK_NUMPAD8 = 0x68,
VK_NUMPAD9 = 0x69,
VK_MULTIPLY = 0x6A,
VK_ADD = 0x6B,
VK_SEPARATOR = 0x6C,
VK_SUBTRACT = 0x6D,
VK_DECIMAL = 0x6E,
VK_DIVIDE = 0x6F,
VK_F1 = 0x70,
VK_F2 = 0x71,
VK_F3 = 0x72,
VK_F4 = 0x73,
VK_F5 = 0x74,
VK_F6 = 0x75,
VK_F7 = 0x76,
VK_F8 = 0x77,
VK_F9 = 0x78,
VK_F10 = 0x79,
VK_F11 = 0x7A,
VK_F12 = 0x7B,
VK_F13 = 0x7C,
VK_F14 = 0x7D,
VK_F15 = 0x7E,
VK_F16 = 0x7F,
VK_F17 = 0x80,
VK_F18 = 0x81,
VK_F19 = 0x82,
VK_F20 = 0x83,
VK_F21 = 0x84,
VK_F22 = 0x85,
VK_F23 = 0x86,
VK_F24 = 0x87,
} UiInputKeyboardKey;
typedef struct UiInputKeyboard {
UiInputKeyboardKey key;
KeyboardModifier modifiers;
} UiInputKeyboard;
typedef enum MouseAction {
MOUSE_ACTION_NONE,
MOUSE_ACTION_RELEASE,
MOUSE_ACTION_LEFT,
MOUSE_ACTION_MIDDLE,
MOUSE_ACTION_RIGHT,
MOUSE_ACTION_SCROLL,
} MouseAction;
typedef struct UiInputMouse {
MouseAction action;
KeyboardModifier modifier;
Point position;
Point scroll;
} UiInputMouse;
typedef struct UiInput {
UiInputType type;
union {
Size resize;
s8 text;
UiInputKeyboard keyboard;
UiInputMouse mouse;
};
} UiInput;
struct UiContext {
Arena* arena;
Arena* arena_prev;
u32 indexed_colors[16];
Size size;
s8 input_text;
UiInputKeyboard input_keyboard;
MouseAction input_mouse_action;
Point input_mouse_position;
Point input_scroll_delta;
bool input_consumed;
u64 focused_item_id;
UiNode* root_first;
UiNode* root_last;
UiNode* attr_node;
UiNode* parent;
usize node_count;
bool autofocus_next;
UiNode** node_map;
usize node_map_shift;
usize node_map_mask;
bool finalized;
};
UiContext* ui_root_create();
void ui_root_setup_indexed_colors(UiContext* ctx, u32 colors[16]);
UiContext* ui_root_reset(UiContext* prev, UiInput input);
s8 ui_root_render(UiContext* ctx);
Size ui_root_get_size(UiContext* ctx);
void ui_consume_input(UiContext* ctx);
bool ui_consume_shortcut(UiContext* ctx, i32 shortcut);
MouseAction ui_input_mouse(UiContext* ctx);
s8 ui_input_text(UiContext* ctx);
UiInputKeyboard ui_input_keyboard(UiContext* ctx);
void ui_container_begin(UiContext* ctx, u64 id);
void ui_container_begin_named(UiContext* ctx, s8 id);
void ui_container_end(UiContext* ctx);
void ui_attr_float(UiContext* ctx, UiFloatSpec spec);
void ui_attr_border(UiContext* ctx);
void ui_attr_padding(UiContext* ctx, Rect padding);
void ui_attr_grid_columns(UiContext* ctx, UiGridColumns columns);
void ui_attr_background_rgba(UiContext* ctx, u32 bg);
void ui_attr_foreground_rgba(UiContext* ctx, u32 fg);
void ui_attr_background_indexed(UiContext* ctx, u32 bg);
void ui_attr_foreground_indexed(UiContext* ctx, u32 fg);
void ui_focus_next_by_default(UiContext* ctx);
bool ui_is_hovering(UiContext* ctx);
bool ui_has_focus(UiContext* ctx);
bool ui_was_clicked(UiContext* ctx);
void ui_label(UiContext* ctx, s8 text);
void ui_styled_label_begin(UiContext* ctx, s8 id);
void ui_styled_label_set_foreground_indexed(UiContext* ctx, u32 fg);
void ui_styled_label_add_text(UiContext* ctx, s8 text);
void ui_styled_label_end(UiContext* ctx);
bool ui_button(UiContext* ctx, s8 text);
bool ui_editline(UiContext* ctx, s8 name);
void ui_textarea(UiContext* ctx, TextBuffer* tb, Size intrinsic_size);
void ui_scrollarea_begin(UiContext* ctx, s8 name, Size intrinsic_size);
void ui_scrollarea_end(UiContext* ctx);
void ui_menubar_begin(UiContext* ctx);
bool ui_menubar_menu_begin(UiContext* ctx, s8 text, c8 accelerator);
bool ui_menubar_menu_item(UiContext* ctx, s8 text, c8 accelerator, i32 shortcut);
void ui_menubar_menu_end(UiContext* ctx);
void ui_menubar_end(UiContext* ctx);

1482
src/ucd.c Normal file

File diff suppressed because it is too large Load Diff

23
src/ucd.h Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "helpers.h"
typedef struct UcdMeasurement {
usize offset;
Point pos;
CoordType movements;
bool newline;
} UcdMeasurement;
UcdMeasurement ucd_measure_forward(s8 str, usize offset, Point pos, CoordType column_stop, CoordType cursor_movement_limit, UcdMeasurement* line_break);
// ucd_measure_backward returns a negative column if it has crossed a newline as it cannot
// possibly know what column its now in without iterating all the way to the start of the line.
// Doing so is the job of the caller as that depends on the way the text is stored
// (a rope for instance will have potentially many segments to iterate through).
UcdMeasurement ucd_measure_backward(s8 str, usize offset, Point pos, CoordType column_stop, CoordType cursor_movement_limit);
usize ucd_newlines_forward(s8 str, usize offset, CoordType* line, CoordType line_stop);
usize ucd_newlines_backward(s8 str, usize offset, CoordType* line, CoordType line_stop);

157
src/vt.c Normal file
View File

@ -0,0 +1,157 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "vt.h"
c8* vt_parse_next_token(VtParserState* state, c8* const beg, c8* const end)
{
c8* it = beg;
state->kind = VT_TOKEN_KIND_PENDING;
while (it != end) {
switch (state->_state) {
case VT_PARSER_STATE_KIND_GROUND:
if (*it == '\x1b') {
state->_state = VT_PARSER_STATE_KIND_ESC;
it += 1;
break;
}
if (*it < '\x20' || *it == '\x7f') {
state->kind = VT_TOKEN_KIND_CTRL;
state->ctrl = *it++;
} else {
state->kind = VT_TOKEN_KIND_TEXT;
state->text.beg = it;
for (; it != end && (*it >= '\x20' && *it != '\x7f'); it += 1) {
}
state->text.len = it - state->text.beg;
}
return it;
case VT_PARSER_STATE_KIND_ESC: {
c8 c = *it++;
switch (c) {
case '[':
state->_state = VT_PARSER_STATE_KIND_CSI;
state->csi.private_byte = 0;
state->csi.final_byte = 0;
while (state->csi.param_count > 0) {
state->csi.param_count -= 1;
state->csi.params[state->csi.param_count] = 0;
}
break;
case ']':
state->_state = VT_PARSER_STATE_KIND_OSC;
state->osc.beg = it;
state->osc.len = 0;
break;
case 'O':
state->_state = VT_PARSER_STATE_KIND_SS3;
break;
case 'P':
state->_state = VT_PARSER_STATE_KIND_DCS;
state->dcs.beg = it;
state->dcs.len = 0;
break;
default:
state->_state = VT_PARSER_STATE_KIND_GROUND;
state->kind = VT_TOKEN_KIND_ESC;
state->esc = c;
return it;
}
break;
}
case VT_PARSER_STATE_KIND_SS3:
state->_state = VT_PARSER_STATE_KIND_GROUND;
state->kind = VT_TOKEN_KIND_SS3;
state->ss3 = *it++;
return it;
case VT_PARSER_STATE_KIND_CSI:
for (;;) {
// If we still have slots left, parse the parameter.
if (state->csi.param_count < array_size(state->csi.params)) {
i32* dst = &state->csi.params[state->csi.param_count];
while (it != end && *it >= '0' && *it <= '9') {
i32 v = *dst * 10 + (*it - '0');
*dst = min(v, 0xffff);
it += 1;
}
} else {
// ...otherwise, skip the parameters until we find the final byte.
while (it != end && *it >= '0' && *it <= '9') {
it += 1;
}
}
// Encountered the end of the input before finding the final byte.
if (it == end) {
return it;
}
c8 c = *it++;
if (c >= '\x40' && c <= '\x7e') {
state->_state = VT_PARSER_STATE_KIND_GROUND;
state->kind = VT_TOKEN_KIND_CSI;
state->csi.final_byte = c;
state->csi.param_count += 1;
return it;
}
if (c == ';') {
state->csi.param_count += 1;
}
if (c >= '<' && c <= '?') {
state->csi.private_byte = c;
}
}
case VT_PARSER_STATE_KIND_OSC:
case VT_PARSER_STATE_KIND_DCS:
for (;;) {
// Tight loop to find any indication for the end of the OSC/DCS sequence.
for (; it != end && *it != '\a' && *it != '\x1b'; it += 1) {
}
state->kind = state->_state == VT_PARSER_STATE_KIND_OSC ? VT_TOKEN_KIND_OSC : VT_TOKEN_KIND_DCS;
// .osc and .dsc share the same address.
state->osc.len = it - state->osc.beg;
// Encountered the end of the input before finding the terminator.
if (it == end) {
return it;
}
c8 c = *it++;
if (c == '\x1b') {
// It's only a string terminator if it's followed by \.
// We're at the end so we're saving the state and will continue next time.
if (it == end) {
state->_state = state->_state == VT_PARSER_STATE_KIND_OSC ? VT_PARSER_STATE_KIND_OSC_ESC : VT_PARSER_STATE_KIND_DCS_ESC;
return it;
}
// False alarm: Not a string terminator.
if (*it != '\\') {
continue;
}
it += 1;
}
state->_state = VT_PARSER_STATE_KIND_GROUND;
return it;
}
case VT_PARSER_STATE_KIND_OSC_ESC:
case VT_PARSER_STATE_KIND_DCS_ESC: {
if (*it == '\\') {
it += 1;
state->kind = state->_state == VT_PARSER_STATE_KIND_OSC_ESC ? VT_TOKEN_KIND_OSC : VT_TOKEN_KIND_DCS;
state->osc.len = it - state->osc.beg;
return it;
}
state->_state = state->_state == VT_PARSER_STATE_KIND_OSC_ESC ? VT_PARSER_STATE_KIND_OSC : VT_PARSER_STATE_KIND_DCS;
break;
}
default:
unreachable();
}
}
return it;
}

50
src/vt.h Normal file
View File

@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "helpers.h"
typedef enum VtParserStateKind {
VT_PARSER_STATE_KIND_GROUND,
VT_PARSER_STATE_KIND_ESC,
VT_PARSER_STATE_KIND_SS3,
VT_PARSER_STATE_KIND_CSI,
VT_PARSER_STATE_KIND_OSC,
VT_PARSER_STATE_KIND_DCS,
VT_PARSER_STATE_KIND_OSC_ESC,
VT_PARSER_STATE_KIND_DCS_ESC,
} VtParserStateKind;
typedef enum VtTokenType {
VT_TOKEN_KIND_TEXT,
VT_TOKEN_KIND_CTRL,
VT_TOKEN_KIND_ESC,
VT_TOKEN_KIND_SS3,
VT_TOKEN_KIND_CSI,
VT_TOKEN_KIND_OSC,
VT_TOKEN_KIND_DCS,
VT_TOKEN_KIND_PENDING,
} VtTokenKind;
typedef struct CsiState {
i32 params[32];
i32 param_count;
c8 private_byte;
c8 final_byte;
} CsiState;
typedef struct VtParserState {
VtParserStateKind _state; // Stores the internal state of the tokenizer.
VtTokenKind kind; // Tells you which one of the union members you need to look at.
s8 text; // The plain text as a string.
c8 ctrl; // The single control character.
c8 esc; // A character that was prefixed by an ESC character.
c8 ss3; // The DCS contents as a string.
CsiState csi; // The CSI parameters and final byte.
s8 osc; // The OSC contents as a string.
s8 dcs; // The DCS contents as a string.
} VtParserState;
c8* vt_parse_next_token(VtParserState* state, c8* const beg, c8* const end);

15
tui.natvis Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<!-- https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2017/debugger/create-custom-views-of-native-objects -->
<Type Name="s8">
<DisplayString>{beg,[len]}</DisplayString>
<StringView>beg,[len]</StringView>
<Expand>
<Item Name="[size]" ExcludeView="simple">len</Item>
<ArrayItems>
<Size>len</Size>
<ValuePointer>beg</ValuePointer>
</ArrayItems>
</Expand>
</Type>
</AutoVisualizer>