DREADWARE

Milestone 3 - The Lexer


The Wrong Answer

I ended Milestone 2 stuck on line endings. Linux and modern Apple machines emit \n, pre-OSX Apple emits \r, and Windows emits \r\n. My printing code walked the buffer one character at a time, so the two-character Windows signal didn't fit. I wrote this at the time:

I could add some special code here to look forwards or backwards an additional character in certain cases, but what I came to realize is that what I am really after is to capture an 'end of line' token.

It turns out I was wrong about what the lexer was for, but it took me some time to discover that. At first, I added an EOL token type, taught the lexer to recognize all three flavors of line ending, and emitted a single clean token for each one. It worked! Printing got simpler! I felt clever for about a day, but then I tried to use it.

What I Was Asking

The questions I needed answered, over and over, were not variations of "what token is this?" They were:

  • What row is the cursor on?
  • Where does row 40 start?
  • How many rows are in this file?
  • What text is on the row above the cursor?

Every one of those questions is a question about lines, and I was asking a lexer, which only knows about syntax. I had one pile of code trying to do two jobs and it was bad at both.

The realization was that these are separate problems that happen to both involve the \n character:

  • Where are the lines? That is a structural question about the file. It has nothing to do with C.
  • What color is this text? That is a syntax question. It has nothing to do with lines.

So I split them. A line table answers the first question and the lexer answers the second. The funny part is that once I split them, the \r\n problem I'd been stuck on for a month simply evaporated.

The Line Table

The line table is the least clever data structure I have ever been happy about. It is a sorted array of the offset of every \n in the buffer:

#define MAX_LINES 65536
typedef struct {
    gap_buffer gb;
    de_cursor  cursor;
    u32 lines[MAX_LINES];  // logical offset of each '\n', ascending
    u32 line_count;
} de_file;

That's it. lines[0] is the byte offset to where the first newline lives, lines[1] the second, and so on. line_count newlines means line_count + 1 rows.

It's fixed-size just like the gap buffer: 65536 lines, 4 bytes each, 256KB per file, and I refuse to open any file bigger. That's a real limit and it might eventually annoy me, but I'd rather ship the annoying version and find out than design a block allocator for a problem I haven't had yet. I actually had started writing a block allocator before backtracking and deciding I was going down a rabbit hole.

Note that the offsets are u32 while the cursor is u64. That is a deliberate mismatch: a 2MB buffer cannot have an offset that doesn't fit in a u32, so paying 8 bytes per line would double the table for nothing.

The whole reason the table earns its keep is this function:

static inline u32
lines_lower_bound(u32* lines, u32 count, u32 pos) {
    u32 lo = 0
    u32 hi = count;
    while (lo < hi) {
        u32 mid = (lo + hi) >> 1;
        if (lines[mid] < pos) {
            lo = mid + 1;
        }
        else {
            hi = mid;
        }
    }
    return lo;
}

The array is sorted because newlines occur in order, which means I never have to sort it, which means the sorted-ness is free and I can use a binary search. The max number of entries I have to look at to find the cursor's row in a 65536-line file is 16.

The index of the first newline at (or after) a position is also the count of newlines before that position, which is also the row number. The row number and the search result are the same integer. I did not design that; it's just true and I get to use it:

static u64
line_start_of(de_file* f, u64 pos) {
    u32 row = lines_lower_bound(f->lines, f->line_count, (u32)pos);
    return (row > 0)
        ? (u64)f->lines[row - 1] + 1
        : 0;
}

static u64
line_end_of(de_file* f, u64 pos) {
    u32 row = lines_lower_bound(f->lines, f->line_count, (u32)pos);
    return (row < f->line_count)
        ? (u64)f->lines[row]
        : gb_text_length(&f->gb);
}

A row starts one byte after the previous row's newline, or at 0 if it's the first row. A row ends at its own newline, or at end-of-file if it's the last row. Both of them are a binary search and an array read.

Killing the Backward Scan

In the past, my cursor code called cursor_line_start, which found the start of a line by walking backwards through the buffer looking for a \n. I even called out my discomfort about recalculating row and column on each draw in a post:

For small files this was fine, but I worried that as the gap buffer text got longer it could slow down.

It turns out that worry was correct. Walking backwards from the cursor is O(n) in the length of the line, which is fine, but CURSOR_UP did it three times and any code that wanted an arbitrary row had to scan from the top of the file. On a long file, holding down the up arrow meant rescanning text I had already scanned thousands of times a second to rediscover something it already knew.

CURSOR_UP is now this:

case CURSOR_UP: {
    u32 row = lines_lower_bound(f->lines, f->line_count, (u32)f->cursor.pos);
    if (row == 0) {
        break;
    }
    u64 prev_start = row_start(f, row - 1);
    u64 prev_len   = f->lines[row - 1] - prev_start;
    u64 target_col = f->cursor.col_intent < prev_len
        ? f->cursor.col_intent
        : prev_len;
    f->cursor.pos = prev_start + target_col;
    f->cursor.row = row - 1;
    f->cursor.col = target_col;
} break;

col_intent works exactly as it did before. The only thing that changed is that finding the previous line is now an array lookup instead of a byte hunt.

There is also a second, better payoff. I previously tracked row and col on the cursor struct incrementally, adjusting them by hand in every direction case because recomputing them was too expensive. That is a cache, and like every cache it was a bug waiting to happen; every new edit operation I write has to remember to fix up row and col, and if I forget one, the cursor silently disagrees with the text. With the table, recomputing the row is cheap, so the cached fields lose most of their value. I've kept them for now because the drawing code reads them constantly, but they're now an optimization I could delete rather than a fact I have to maintain. That's a much better place to be.

The cost, of course, is that the table has to be right. Every insert or delete of a \n has to update it, and every edit at all has to shift the offsets after it. I have that working, but I don't love the code yet, so I'm going to leave it for its own post once I've lived with it.

The Lexer

With lines handled elsewhere, the lexer got to be small and single-minded.

The token types:

typedef enum {
    TOKEN_DEFAULT,
    TOKEN_KEYWORD,
    TOKEN_TYPE,
    TOKEN_IDENTIFIER,
    TOKEN_NUMBER,
    TOKEN_STRING,
    TOKEN_CHAR_LIT,
    TOKEN_COMMENT,
    TOKEN_PREPROCESSOR,
    TOKEN_PUNCTUATION,
    TOKEN_OPERATOR,
} token_type;

typedef struct {
    token_type type;
    u64        start;
    u64        end;
} de_token;

There is no TOKEN_EOL. There is no TOKEN_WHITESPACE either. A token is a type and a pair of offsets into the buffer. It does not own any characters because the characters are already sitting in the gap buffer and copying them somewhere else would be silly.

Here is the top of the loop, and it is where the whole \r\n saga ends:

u64 i = start_pos;
while (i < end_pos && count < max_tokens) {
    u8 c = gb_get_char(&f->gb, i);
    if (c == '\r' || c == ' ' || c == '\t') {
        i++;
        continue;
    }
    if (c == '\n') {
        in_preprocessor = 0;
        i++;
        continue;
    }
    // ...

That's the answer to the question I asked in Milestone 2. \r is skipped, \n is skipped, and neither produces a token. I spent real time worrying about how to represent a two-character line ending as one token and the resolution was that the lexer does not need to represent line endings at all. Whether the file says \n or \r\n or \r, the lexer skips the bytes and keeps going, and the answer is identical. The line table doesn't care either: it records the offset of the \n, and a \r sitting in front of it is just a character on the end of the line that happens to be invisible.

The only thing \n does in the lexer is clear a in_preprocessor flag because #define ends at the end of a line. The rest of the loop is what you'd expect: recognize a thing, emit a span, continue.

if (c == '/' && i + 1 < len && gb_get_char(&f->gb, i + 1) == '/') {
    u64 start = i;
    while (i < len && gb_get_char(&f->gb, i) != '\n') {
        i++;
    }
    out[count].type  = TOKEN_COMMENT;
    out[count].start = start;
    out[count].end   = i;
    count++;
    continue;
}

if (c == '"') {
    u64 start = i++;
    while (i < len) {
        u8 sc = gb_get_char(&f->gb, i);
        if (sc == '\\') {
            i += 2;
            continue;
        }
        if (sc == '"' || sc == '\n') {
            i++;
            break;
        }
        i++;
    }
    out[count].type  = TOKEN_STRING;
    out[count].start = start;
    out[count].end   = i;
    count++;
    continue;
}

Two notes on the string case:

  • The backslash skips two characters so that "he said \"hi\"" doesn't terminate early.
  • An unterminated string stops at the newline instead of running to the end of the file, because while I'm typing, every string is unterminated for a moment, and I don't want the rest of the file to flash red every time I type a quote.

Identifiers are the only place the lexer knows what language it's reading:

if (is_ident_start(c)) {
    u64 start = i;
    while (i < len && is_ident_char(gb_get_char(&f->gb, i))) {
        i++;
    }
    token_type type = TOKEN_IDENTIFIER;
    if (in_preprocessor) {
        type = TOKEN_PREPROCESSOR;
    }
    else if (word_list_contains(&keywords, word, word_len)) {
        type = TOKEN\_KEYWORD;
    }
    else if (word_list_contains(&types, word, word_len)) {
        type = TOKEN_TYPE;
    }
    // ...

Scan the whole word then look it up. Keywords and types are just two lists of strings, which means C isn't compiled into the lexer and I can add another language later by adding another list. That is deliberate and it is as far as I'm taking it for now.

Note also that the function takes a start_pos and an end_pos rather than tokenizing the file. I only ever need colors for the rows that are on screen, and there is no reason to lex 60,000 lines to draw 50 of them. The line table is what makes that possible! Given a row, I know its byte range, so I can lex exactly the range I'm about to draw.

The One Hard Part

Lexing an arbitrary range has a catch, and I want to be honest that I have not fully solved it.

To lex from the middle of a file, you need to know what was already happening at that point. More specifically we need to know "are we inside a block comment?" The raw bytes cannot tell you because the /* might be 400 lines above the viewport. If you guess wrong, an entire screen of code renders as a comment, or an entire comment renders as code.

My current answer is to store one byte of lexer state per line, alongside the line table, where bit 0 means "this line starts inside a block comment":

u8 line_states[MAX_LINES];

To lex a range, I look up the state at the first line and start there:

u32 start_line = lines_lower_bound(f->lines, f->line_count, (u32)start_pos);
u8  state      = f->line_states[start_line];
if (state) {
    u64 start = i;
    while (i + 1 < len) {
        if (gb_get_char(&f->gb, i) == '*' && gb_get_char(&f->gb, i + 1) == '/') {
            i += 2;
            state = 0;
            break;
        }
        i++;
    }
    // ...

This works and it's fast. Keeping that array correct as the file gets edited is a different story, and it has already produced the most interesting bug I've hit on this project so far. I'm going to save that for a future post, because it deserves one.

Drawing

print_token from Milestone 2 barely changed. It had a TODO on it asking for exactly this:

// TODO: determine token type and select colors

Now the token type arrives as an argument, and the color is a table lookup:

static COLORREF
token_color(token_type type) {
    switch (type) {
        case TOKEN_KEYWORD:      return COLOR_KEYWORD;
        case TOKEN_TYPE:         return COLOR_TYPE;
        case TOKEN_NUMBER:       return COLOR_NUMBER;
        case TOKEN_STRING:       return COLOR_STRING;
        case TOKEN_CHAR_LIT:     return COLOR_CHAR_LIT;
        case TOKEN_COMMENT:      return COLOR_COMMENT;
        case TOKEN_PREPROCESSOR: return COLOR_PREPROCESSOR;
        case TOKEN_PUNCTUATION:  return COLOR_PUNCTUATION;
        case TOKEN_OPERATOR:     return COLOR_OPERATOR;
        case TOKEN_IDENTIFIER:
        case TOKEN_DEFAULT:
        default:                 return COLOR_TEXT;
    }
}

Those are #defines pointing at RGB() values for now. Making them loadable from a file is the Color Themes item, and it can wait. I use internal defaults for now.

The draw loop no longer walks the buffer at all. It asks the line table which rows are visible, asks the lexer for the tokens in that byte range, then draws them. Since the tokens are byte spans and whitespace isn't a token, the gaps between tokens are exactly the whitespace, so I advance x by the width of the gap and don't draw anything there.

I also finally get line numbers, which was sitting under Code Navigation on the plan. line_count is a field. The row of the top of the viewport is a binary search. Line numbers are a for loop and a counter. It took about ten minutes, and it took ten minutes because of the table, not in spite of it.

What I Learned

I spent a month stuck on \r\n and the fix was simply to notice that I had pointed the question at the wrong system. The lexer was never the thing that cared about lines. Once the line table existed, the lexer got to skip those bytes entirely, and a problem I had been circling turned out not to be a problem at all.

I don't think I could have seen that from the outside. I had to build the EOL token, use it, and find it useless. That's the second time on this project that writing the usage code taught me the design, and I suspect it won't be the last.

Plan Update

Next up is File I/O, and I'm moving it ahead of the rest of Code Navigation on purpose. I am still writing this editor in notepad, and the original milestone I set for myself was feature parity with notepad. Searching and jumping can wait. Being able to open and save a file is the thing standing between me and using this thing to write itself.

Google Fonts


Simplicity First

I strongly believe that the modern web is a needlessly complex and wasteful mess that has drastically reduced software dependability. I abhor loading screens and scroll-stutter on webpages while using a computer that can run Cyberpunk 2077 4k@60fps. When I first started to draft this blog, I adopted the exceptionally minimal style I've seen on other blogs and sites that I admire. However, the simplicity of that style appeals to a small niche (mostly), and I want this blog to be more welcoming to those that are used to the modern web.

In order to find middle ground, I write all of my posts in markdown, run it through a custom converter, and spit out a single HTML file with a single style sheet. My hope is that the absence of scripts and external dependencies help keep things simple and fast while still maintaining a modern-enough feel for a casual, modern user.

That said, I did have an external dependency and I just recently eliminated it: a reference to fonts.googleapis.com used to source the font for the DREADWARE logo. The font that I use is one that is not typically on a user's machine by default, and I didn't want to use a large, dynamic image for something as simple as text. A commonly accepted solution to this is to request the font from somewhere, which I did in the HTML like this:

 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 <link href="https://fonts.googleapis.com/css2?family=Zen+Dots&display=swap"
          rel="stylesheet">

This worked and was convenient so I added it and forgot about it.

Tracking

I am well aware of Google's reputation for tracking, and I assumed that Google would be garnering some sort of analytics from the font requests, but I was reading some stuff recently that gave me pause. Being a non-expert in modern web development, I did not consider that retrieving the font could possibly result in script execution. While external script execution should not theoretically be possible by calling the fonts API within a tag, Google is very clever and I assume they are able to do pretty much whatever they want by cobbling together data from several sources and exploiting loopholes.

Since all I am doing is downloading a font, the font has a fairly permissive license, and I want to remove every external dependency that I possibly can, I changed the source of the font to be locally hosted. I now upload the font directly and reference it in the style sheet as follows:

@font-face {
    font-family: 'Zen Dots';
    src: url('fonts/ZenDots-Regular.woff2') format('woff2'),
         url('fonts/ZenDots-Regular.ttf')   format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

Beyond eliminating the Google tracking and risk of external script execution, this has the added benefit of referencing a woff2 formatted font which is more compact than a truetype font, making the blog theoretically faster to load than before. I fallback to the truetype version if the user has a browser that doesn't support woff2.

Microsoft Strikes Again


Windows Activation

As noted in my first few posts, I am developing software on an air-gapped (read: internet-less) PC running Windows 7. I am using a legitimate copy of Windows 7 that I bought years ago with an authentic cd-key that gave me no trouble during installation. I had totally forgotten about "activation" and went about my business for months with no trouble, until today.

When I started working I was greeted with a popup telling me that my activation period had expired and I needed to activate Windows now. Ok, sure let's do that...

oh no

It wants me to activate Windows online. I can't activate it online because...

  1. There is no wireless card in this thing
  2. It's extraordinarily tedious to hook this PC up near my router for ethernet access
  3. Not being connected to the internet is a feature of this device

One of the options is to retype my cd-key, which seems reasonable enough, although this entire process is unreasonable. After entering my cd-key, I am greeted by a challenge screen:

oh no again

I obfuscate the numbers there, but it's a collection of 54 numbers, 6 in each of the 9 boxes. It tells me to call the automated number and enter the excruciatingly long set of numbers, presumably so it can read back another set of ridiculous numbers for me to enter. I think this is stupid, but I call anyway since the SMS number is "not available."

I am greeted by an automated system that asks me many questions about why I'm calling and what I need help with. Once it realizes that I want to activate Windows over the phone, it tells me to go to a website instead and hangs up on me. It wants me to visit aka.ms/aoh which expands to https://visualsupport.microsoft.com/ apparently. I am doing this on my phone, since I need to read these numbers from my PC to get access.

I am first met with a "Security Verification" challenge, which is a captcha. It presents a train-track with a train and 4 stops, each with a symbol. It then shows me one of the symbols flipped upside-down and tells me to use the arrows to move the train to the correct stop marked by the symbol. Once I finish this preschool matching task, I press submit and I get an error, of course.

error

Reloading the challenge is the only option, so I do it. I get the same captcha with different symbols. I solve it again and get an error again. After 3 failures, I assume it's because I am using a browser they don't expect and I try again with Safari. This time I pass the captcha on the first try. I now feel like a captcha expert, having correctly solved the train riddle 5 times in a row. My reward is another challenge screen where I finally get to enter the 54 numbers from earlier.

After entering the numbers, it spits out 54 different numbers that I must enter on the PC. The whole time I am thinking to myself, "the chances that I mistyped a number on my phone is pretty high, the chances that I am mistyping a number now, from the results, is also high," so I don't expect it to work.

Miraculously, it does somehow work on the first try. I get a message that Windows 7 is now activated and I start coding, having wasted nearly 20 minutes of my day.

success

I am hopeful this activation sticks and does not need re-activation in the future, but I fear it's likely to come up again since I'm not using the internet. Even software from 2009 seems to think that having constant, uninterrupted internet access is a given.

Refactor


Ergonomics

As I am iterating more and needing faster feedback on my code, I have updated a few things in my build.bat. The biggest change is switching the default build from being RELEASE to INTERNAL (meaning debug). It seems small, but it helps. Beyond saving on typing, it means that building a release version is now done intentionally, using build release as the command. Building a debug version is certainly the most common case while developing, so it is now the default case.

In the future, I may adapt the code to be split between .exe and .dll to allow for hot-reloading and even faster iteration, but I haven't needed it yet.

Another ergonomic change I implemented was splitting out generic, common code into a header. This new header now resembles a header that I reuse often, and has the definitions of useful typedefs, macros, and functions that end up getting used by most of my programs. Some of these things are obvious, like my primitive type definitions:

// signed
typedef char      i8;
typedef short     i16;
typedef int       i32;
typedef long long i64;

// unsigned
typedef unsigned char      u8;
typedef unsigned short     u16;
typedef unsigned int       u32;
typedef unsigned long long u64;

// address
typedef char* ptr;

// boolean
typedef int b32;

// floating point
typedef float  f32;
typedef double f64;

But other things are more ambiguous. For instance, I am pretty sure that common memory functions should be in this header, like my mem_copy, but what about my mem_arena code? I do tend to include mem_arena in most of my code bases, but I can imagine a case where I might want a totally different memory strategy on a platform in the future. For now, I've included it in the header since it will get reused, but that might change in the future.

Further Changes

As I was writing and testing my lexer, I kept running into frustration when navigating the text that I was writing for testing. After a few iterations, I got fed up and thought harder about how I handle the cursor position.

Since adding new cursor code required changes throughout the code base, I took the opportunity to refactor many of the functions and win32 message handling. I won't paste the entire program here, but I wanted to note it since many function signatures have changed; some have been deleted and some have been added. Don't be surprised if you see code here that doesn't match up exactly with what was shown in previous posts.

A Smarter Cursor

My first pass at a new cursor left the editor recalculating the row and column of the cursor on each draw call. For small files this was fine, but I worried that as the gap buffer text got longer it could slow down. On top of that, since the cursor only moves when the user intervenes, there is no technical reason that I shouldn't just be keeping track of it as actions occur. I landed on adding a cursor type to the editor:

typedef struct {
	u64 pos;
	u64 row;
	u64 col;
	u64 col_intent;
} de_cursor;

The position, called pos here, is the same raw offset into the text that I was keeping track of before. New items row and col are straight forward, but col_intent is special, representing the column on which the user intended the cursor to appear.

If you imagine navigating a file with the arrow keys, pressing up or down should move the cursor exactly 1 row while keeping the column the same as if it was a grid. In cases where the text length of the lines differ, this presents a problem. For example, if the cursor is on a row with 120 characters and the line above has only 40 characters, we would expect the cursor to move up 1 row and then appear in the column at the end of the text of the shorter line. Then when the user presses down again, the cursor should be back to the exact spot it was in previously, which might be a different logical column.

Let's take a look at the cursor movement function so I can explain how I handle it:

typedef enum {
	CURSOR_LEFT,
	CURSOR_DOWN,
	CURSOR_UP,
	CURSOR_RIGHT,
} cursor_dir;

static void cursor_move(de_file* f, cursor_dir dir) {
	...
}

Not that I am passing in a file, which contains a cursor struct as described above and the gap buffer shown on the screen. I also pass an enum value to represent which direction the user is moving the cursor. I do this in a function so that I don't lose track of the data I need to change at each call site. Having it all in once place reduces the chances of mistakes which reduces potential debugging risk later.

The next thing I do is process the direction in a case statement. The left and right movement is pretty straight forward:

switch (dir) {
	case CURSOR_LEFT: {
		if (f->cursor.pos == 0) {
			break;
		}
		f->cursor.pos--;
		if (gb_get_char(f->buf, f->cursor.pos) == '\n') {
			f->cursor.row--;
			f->cursor.col = cursor_col_at(f->buf, f->cursor.pos);
			}
		else {
			f->cursor.col--;
		}
		f->cursor.col_intent = f->cursor.col;
	} break;
        
	case CURSOR_RIGHT: {
		if (f->cursor.pos >= len) {
			break;
		}
		char c = gb_get_char(f->buf, f->cursor.pos);
 		f->cursor.pos++;
		if (c == '\n') {
			f->cursor.row++;
			f->cursor.col = 0;
			}
			else {
				f->cursor.col++;
			}
			f->cursor.col_intent = f->cursor.col;
	} break;
            

I increment and decrement the position as I did before, but now I handle a case when the user presses left while at the start of a row, or presses right while at the end of a row. In those cases, I expect to hit a carriage return and then increment or decrement the row of the cursor and snap the column to the start or end of the line.

This cursor behavior is typical and is part of editors like notepad and Visual Studio. I might change this in the future since I'm not sure I like that behavior, even though it is familiar. I prefer that pressing left at the start of the line, or right at the end of the line, simply keeps the cursor in it's current position. If I want to make it configurable it might be a little extra effort, but for now I'll keep the feature set as close to notepad as I can.

Let's take a look at the up and down cases next, since col_intent will come into play:

case CURSOR_UP: {
	if (f->cursor.row == 0) {
		break;
	}
	u64 start = cursor_line_start(f->buf, f->cursor.pos);
	u64 prev_line_end = start - 1;
	u64 prev_line_start = cursor_line_start(f->buf, prev_line_end);
	u64 prev_line_len   = (prev_line_end - prev_line_start);

	u64 target_col = f->cursor.col_intent < prev_line_len
		? f->cursor.col_intent
		: prev_line_len;

	f->cursor.pos = prev_line_start + target_col;
	f->cursor.row--;
	f->cursor.col = target_col;
} break;

case CURSOR_DOWN: {
	u64 line_end = cursor_line_end(f->buf, f->cursor.pos);
	if (line_end >= len) {
		break;
	}
	u64 next_line_start = line_end + 1;
	u64 next_line_len   = cursor_line_len(f->buf, next_line_start);
    
	u64 target_col = f->cursor.col_intent < next_line_len
		? f->cursor.col_intent
		: next_line_len;

	f->cursor.pos = next_line_start + target_col;
	f->cursor.row++;
	f->cursor.col = target_col;
} break;

Looking at CURSOR_UP, the first thing I do is check that we aren't at the top of the file already, then I start looking at the line endings. I find the '\n' that should end the line above, then measure the start and end offsets of that line of text. I do this so that when I move the cursor up it doesn't move directly on a grid but instead takes the text length of the line into account. The column to move to is represented as target_col and that is where I take the column intent into consideration. Essentially, if the intended column exists in the row above, I just move up 1 row and I'm done. However, if there is less text on the line above, I want to snap to the end of that line. I keep track of the intended column though, so that if the user presses down I go back to the column the cursor previously visited. That is how notepad works and it's what users tend to expect; pressing up and then down should leave the cursor back in the position it was in before, as if we performed an undo. If I didn't keep track of that intent, pressing down would still move the cursor down, but to a different column than we originated which would feel weird. The user doesn't expect horizontal cursor movement from an up or down arrow press.

Next Steps

Since the cursor now lets me navigate the code in all directions, I feel more comfortable continuing with the lexer. I will keep the plan the same and update the next milestone once I am happy with how it works.

Milestone 2 - Gap Buffers


Text Buffers

At the conclusion of Milestone 1, I could type text in a line and have it show up in the window, and could even backspace from the end of the string. Next I want to add a cursor that can move left and right with the ability to insert and delete text anywhere in the string.

Something that came up in my research of text editors was the concept of a gap buffer. A gap buffer is a segment of memory, representing a string, that is subdivided into three regions: the string that appears before cursor, a buffer of unused space, and the remainder of the string that comes after the cursor. Partitioning the memory this way allows a user to type, inserting characters at the cursor (within the gap region) without having to move memory on every keystroke.

Without any references, this is the gap buffer scheme I started with to give me a predictable, fixed-size buffer:

// constants
#define GAP_BUFFER_DATA_LENGTH	MB(2)

// types
typedef struct {
	i8  data[GAP_BUFFER_DATA_LENGTH];
	u64 gap_start;
	u64 gap_end;
} gap_buffer;

The obvious constraint here is that I am enforcing a limit of 2MB on each buffer, which might be a problem down the road. However, I'm taking a very iterative approach so I will handle dynamic/block allocations at a later time. I am doing this to try to avoid over-designing the system before I've even used it.

"Always write the usage code first." - Unknown wise man

Something I picked up along the way was the idea that the usage code should be written before the code itself. This was counterintuitive to me when I first heard it since many IDEs, and certainly the compiler, will complain if you are using code that doesn't exist yet. The red squiggles in Visual Studio certainly guide a developer to write a function first and use it second, but this can lead to bad design due to incorrect assumptions. If the usage code is written first, the API generally becomes much more clear. Instead of trying to imagine all of the different ways a function could be possibly be used in a system that doesn't exist, it makes much more sense to write the usages of the function in the system where they are needed, and then design the API based on what data is present and needed at the time of usage. This generally makes API design simple and concise.

Here are the functions I added to get the gap buffer working:

// functions
static void
gb_reset(gap_buffer* gb) {
	gb->gap_start = 0;
	gb->gap_end = GAP_BUFFER_DATA_LENGTH;
}

static u64
gb_text_length(gap_buffer* gb) {
	return GAP_BUFFER_DATA_LENGTH - (gb->gap_end - gb->gap_start);
}

static void
gb_move_gap_to_cursor(gap_buffer* gb, u64 cursor) {
	// move gap left
	if (cursor < gb->gap_start) {
		u64 len = gb->gap_start - cursor;
		mem_move(gb->data + cursor, gb->data + gb->gap_end - len, len);
		gb->gap_end   -= len;
		gb->gap_start -= len;
	}
	// move gap right
	else if (cursor > gb->gap_start) {
		u64 len = cursor - gb->gap_start;
		mem_move(gb->data + gb->gap_end, gb->data + gb->gap_start, len);
		gb->gap_start += len;
		gb->gap_end   += len;
	}
}

static void
gb_insert(gap_buffer* gb, u64 cursor, char c) {
	gb_move_gap_to_cursor(gb, cursor);
	if (gb->gap_start == gb->gap_end) {
		return;
	}
	gb->data[gb->gap_start++] = c;
}

static void
gb_backspace(gap_buffer* gb, u64 cursor) {
	if (cursor == 0) {
		return;
	}
	gb_move_gap_to_cursor(gb, cursor);
	gb->gap_start--;
}

static void
gb_delete(gap_buffer* gb, u64 cursor) {
	u64 length = gb_text_length(gb);
	if (length <= 0 || cursor >= length) {
		return;
	}
	gb_move_gap_to_cursor(gb, cursor);
	gb->gap_end++;
}

Most of those functions are pretty straight forward, leveraging gb_move_gap_to_cursor to manage the gap as a user types. It takes an offset that I am calling "cursor" as the point of future insertion. That means I will copy all of the memory within a span to the left or right of the buffer, based on the the cursor's position relative to the existing gap. The cursor does not know about the gap, so it's an offset of characters within the string.

To connect the gap buffer to keyboard events, I simply call the functions within my WndProc and replace the old instances of insert_character:

case WM_KEYDOWN: {
	data = (ded_data*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
	switch (wParam) {
		case VK_LEFT: {
			if (data->cursor > 0) {
						data->cursor--;
			}
		}
		break;
		
		case VK_RIGHT: {
			if (data->cursor < gb_text_length(&data->gb)) {
				data->cursor++;
			}
		}
		break;

		case VK_DELETE: {
			gb_delete(&data->gb, data->cursor);
		}
		break;

		case VK_BACK: {
			gb_backspace(&data->gb, data->cursor);
			if (data->cursor > 0) {
				data->cursor--;
			}
		}
		break;

		case VK_ESCAPE: {
			PostMessage(hwnd, WM_CLOSE, 0, 0);
		}
		break;

	}
	InvalidateRect(hwnd, NULL, TRUE);
}
return 0;

case WM_CHAR: {
	data = (ded_data*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
	switch (wParam) {
		case VK_TAB:{
			gb_insert(&data->gb, data->cursor, '\t');
			data->cursor++;
		}
		break;

		default: {
			if (wParam >= 32 && wParam <= 126) {
				gb_insert(&data->gb, data->cursor, (char)wParam);
				data->cursor++;
			}
		}
	}
	InvalidateRect(hwnd, NULL, TRUE);
}
break;

Printing

There is, of course, a lot of room for optimizations here, but I don't think most of this code is going to end up staying around very long. This became clear as I started writing the print function for the gap buffer. Here is what I started with:

static i32
print_token(HDC hdc, char* s, u64 len, i32 x, i32 y) {
	// TODO: determine token type and select colors
	SetBkColor(hdc, COLOR_BACK);
	SetTextColor(hdc, COLOR_TEXT);

	SIZE size;
	if (*s == '\t') {
		TextOutA(hdc, x, y, "    ", 4);
		GetTextExtentPoint32A(hdc, "    ", 4, &size);
	}
	else {
		TextOutA(hdc, x, y, s, len);
		GetTextExtentPoint32A(hdc, s, len, &size);
	}
	
	return size.cx;
}

static void
gb_draw(HDC hdc, gap_buffer* gb, u64 cursor) {
	i32 x = 0;
	i32 y = 0;

	char token[255];
	u64  token_length = 0;
	for (u64 i = 0; i < GAP_BUFFER_DATA_LENGTH; i++) {
		if (i == gb->gap_start) {
			i = gb->gap_end - 1;
			continue;
		}
		switch (gb->data[i]) {
			case ' ' :
			case '\t': {
				if (token_length > 0) {
					x += print_token(hdc, token, token_length, x, y);
				}
				token_length = 0;
				x += print_token(hdc, gb->data + i, 1, x, y);
			}
			break;

			// TODO: handle /r/n
			// TODO: increment Y
			case '\r':
			case '\n': {
				if (token_length > 0) {
					x += print_token(hdc, token, token_length, x, y);
				}
				token_length = 0;
			}
			break;

			default: {
				token[token_length++] = gb->data[i];
			}
		}
	
		
	}

	if (token_length > 0) {
		print_token(hdc, token, token_length, x, y);
	}
}

At first, I was just printing the entire buffer (minus the gap), but I wanted to make sure I have the ability to color different tokens as most editors do. To do that I tried to divide the string up as I went where a token was defined based on surrounding whitespace. When I added tabs I noticed that they would not print as expected. It seems TextOutA does not support tabbed printing, so I needed to add that as a special case which you can see in print_token.

Once tabs were handled, I thought line endings should be next and that is where I ran into some trouble. Different systems emit different line ending characters. For example, most modern Linux and Apple devices use \n to signal end-of-line, Apple before OSX uses \r alone, and Windows has seemingly always used \r\n. The Windows use case is what throws a wrench in my code, since it's a 2-character signal and I am only walking 1-character at a time. I could add some special code here to look forwards or backwards an additional character in certain cases, but what I came to realize is that what I am really after is to capture an 'end of line' token.

Plan Update

I can see that I am very slowly starting to build a lexer, or at least I am finding the use-case for one. This seems like a great time to update and revise my plan:

  • Win32 Window
    • Message Loop
    • Back Buffer + Blit
    • Simple screen painting
  • Font + Text Rendering
    • Evaluate stb_truetype integration
    • Render hardcoded text to screen
  • Typing, Cursor and Gap Buffer
    • Translate keystrokes to text
    • Insert and delete text
    • Blinking cursor
  • Lexer
    • Handle new lines
    • Support syntax highlighting
  • Code Navigation
    • Blinking cursor
    • Show line numbers
    • Implement ability to search for, and jump to, text
  • Hotkeys
    • Copy
    • Cut
    • Paste
    • Support common code-related commands
  • File I/O
    • Open files on disc
    • Save files to disc
  • Multi-pane
    • Support having two files open at once
    • Support having the same file open twice with independent cursors
  • External Process
    • Support running a BAT from a hotkey to compile code
    • Support debugger integration (jump to line)
  • Color Themes
    • Support custom colors

Milestone 1


First Steps

Earlier, I told you why I’m insane enough to write my own code editor and explained my approach. Today, I'm sharing some of the code. It’s starting to feel like I might actually get this thing to work.

First, I had to setup a compiler. Since I recently tried and failed to get the Microsoft compiler working, I tried GCC next. This was actually a great experience! I managed to find a version of GCC that works with Windows 7 (GCC 14.2.0) and grabbed it from winlibs.com. I opted for the version without LLVM/Clang/LLD/LLDB, downloaded the zip, extracted the binaries to a folder on my machine, and added it to my PATH. I could successfully hit it with gcc --version. This is how software should be distributed! Why can't Microsoft distribute their compiler this way?

I then wrote a tiny C program:

int main(int argc, char* argv[]) {
	return 0;
}

...and a Windows batch file to automate compilation:

@echo off
setlocal

set FLAGS=-Wall -Wextra -Werror -std=c99

echo.
if "%~1"=="debug" (
	echo [DEBUG BUILD]
	set FLAGS=-Og -g -DINTERNAL_BUILD %FLAGS%
	set OUTPUT_DIR=.\bin\debug\
) else (
	echo [RELEASE BUILD]
	set FLAGS=-O2 -s %FLAGS%
	set OUTPUT_DIR=.\bin\release\
)
if not exist "%OUTPUT_DIR%" (
	mkdir "%OUTPUT_DIR%"
)

echo Invoking cGCC...
pushd %OUTPUT_DIR%
gcc %FLAGS% -o de.exe ..\..\src\dreadedit.c
if %ERRORLEVEL% equ 0 (
	echo Build Success
) else (
	echo Build Failure
)
popd

echo Cleaning temp files...
pushd "%OUTPUT_DIR%"
del *.o *.obj 2> NUL
popd

endlocal
exit /b 0

This let me use build or build debug commands to build my executable.

  • Prereq: get a compiler installed and working

The next step in my plan was to create and open a window. I found that win32 has some built-in functions for text rendering which I leveraged to present some hardcoded text:

milestone

At this point I had most of items 1 and 2 of my plan complete:

  • Win32 Window
    • Message Loop
    • Back Buffer + Blit
    • Simple screen painting
  • Font + Text Rendering
    • Evaluate stb_truetype integration
    • Render hardcoded text to screen

I skipped looking at stb_truetype since win32 has built-in text rendering. I may revisit it later.

Next up was handling input. I made a simple character buffer and added handling of WM_KEYDOWN and WM_CHAR messages to get some text into the buffer to prove that typing works:

int position = 0;
char buffer[255];
void insert_character(char c) {
	buffer[position++] = c;
}
TextOutA(hdc, 0, 0, buffer, length);

However, it wasn't drawing to the screen as I expected. I fired up RemedyBG and ran into a problem: breakpoints were not working. I suspected the debugger was not able to load the debug symbols, so I started to investigate.

I didn't see a .pdb generated, but GCC supposedly embeds debug stuff into the .exe itself, so I didn't think much of it. After reading, it seems RemedyBG requires the information within a .pdb to get breakpoints and variable inspection working. I looked to see if GCC could output a .pdb, and the answer was: yes it can! With -g -gcodeview flags it spits out a .pdb. However, RemedyBG was still not able to add breakpoints. I did some more reading, asked a few questions to an LLM, and discovered that GCC can output a .pdb, but the generated debug information is not sufficient for RemdyBG. I could find no obvious way to coerce GCC into providing what RemedyBG requires.

At this point I had a compiler that worked but no debugger, which was going to become an increasingly nasty problem for me. I looked at the RAD Debugger, but from what I can tell it suffers from the same problems as RemedyBG without a valid Microsoft .pdb near the executable.

Luckily, my trusty LLM told me that Clang is able to output a proper .pdb. My decision to download GCC without Clang now seemed foolish. I looked for a version of Clang for Windows 7, downloaded LLVM-15.0.7-win64.exe and installed it.

This was the first time I've used Clang, so I wasn't sure what I was in for. I'm happy to say that going from GCC to Clang was pain free. I updated a couple lines in my .bat and Clang generated a .pdb that RemedyBG could use. After a single debug session I found that I was not calling InvalidateRect(hwnd, NULL, TRUE); in the right place, so I wasn't seeing text because Windows was not redrawing the graphics for me. Once I added that, I was in business!

Current State

Here is where I am today:

  • Typing, Cursor and Gap Buffer
    • Translate keystrokes to text
    • Insert and delete text
    • Blinking cursor

I'll dump my current .bat here for posterity:

@echo off
setlocal

set FLAGS=-Wall -Wextra -Werror -std=c99
set FLAGS=%FLAGS% -target x86_64-w64-windows-gnu -fuse-ld=lld
set FLAGS=%FLAGS% -lgdi32 -luser32 -mwindows

echo.
if "%~1"=="debug" (
	echo [DEBUG BUILD]
	set FLAGS=%FLAGS% -Og -g -gcodeview -Wl,--pdb=de.pdb -DINTERNAL_BUILD
	set OUTPUT_DIR=.\bin\debug\
) else (
	echo [RELEASE BUILD]
	set FLAGS=-O2 -s %COMPILER_FLAGS%
	set OUTPUT_DIR=.\bin\release\
)
if not exist "%OUTPUT_DIR%" (
	mkdir "%OUTPUT_DIR%"
)

echo Invoking compiler...
pushd %OUTPUT_DIR%
clang %FLAGS% -o de.exe ..\..\src\dreadedit.c
if %ERRORLEVEL% equ 0 (
	echo Build Success
) else (
	echo Build Failure
)
popd

echo Cleaning temp files...
pushd "%OUTPUT_DIR%"
del *.o *.obj 2> NUL
popd

endlocal
exit /b 0

Here is what I have at the top of my .c file to handle GCC and Clang warnings, along with MSC since that is what I am accustomed to using:

//////////////////////////////////////////////////////////////////////////////////////////
// COMPILER
//////////////////////////////////////////////////////////////////////////////////////////

#if defined(__GNUC__)
#   define COMPILER_GCC
#elif defined(__clang__)
#  define COMPILER_CLANG
#elif defined(_MSC_VER)
#   define COMPILER_MSC
#else
#   error compiler not supported
#endif

#if defined(COMPILER_GCC)
#  if defined(INTERNAL_BUILD)
#    pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#    pragma GCC diagnostic ignored "-Wunused-function"
#    pragma GCC diagnostic ignored "-Wunused-parameter"
#    pragma GCC diagnostic ignored "-Wunused-variable"
#    pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#  endif
#  define inline inline __attribute__((always_inline))
#elif defined(COMPILER_CLANG)
#  if defined(INTERNAL_BUILD)
#    pragma clang diagnostic ignored "-Wmissing-field-initializers"
#    pragma clang diagnostic ignored "-Wunused-function"
#    pragma clang diagnostic ignored "-Wunused-parameter"
#    pragma clang diagnostic ignored "-Wunused-variable"
#    pragma clang diagnostic ignored "-Wunused-but-set-variable"
#  endif
#  define inline inline __attribute__((always_inline))
#elif defined(COMPILER_MSC)
#  pragma warning(disable:4201) // nameless struct/union
#  if defined(INTERNAL_BUILD)
#    pragma warning(disable:4100) // unreferenced formal parameter
#    pragma warning(disable:4101) // unreferenced local variable
#    pragma warning(disable:4127) // conditional expression is constant
#    pragma warning(disable:4189) // local variable is initialized but not referenced
#    pragma warning(disable:4505) // unreferenced local function has been removed
#    pragma warning(disable:4702) // unreachable code
#  endif
#  define inline __forceinline
#endif

I also want to share how I am currently processing keystrokes. I use WM_KEYDOWN to filter some non-character keys that I will likely need in the future. I then let Windows translate the keystrokes for me and capture characters with WM_CHAR as seen here:

#define COLOR_BACK RGB(0,0,0)
#define COLOR_TEXT RGB(255,255,255)

LRESULT CALLBACK
WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
		case WM_CLOSE:
			DestroyWindow(hwnd);
			return 0;
		case WM_PAINT: {
				PAINTSTRUCT ps;
				HDC hdc = BeginPaint(hwnd, &ps);
				
				HBRUSH hBrush = CreateSolidBrush(COLOR_BACKGROUND);
				RECT rc;
				GetClientRect(hwnd, &rc);
				FillRect(hdc, &rc, hBrush);
				DeleteObject(hBrush);
	
				SetBkColor(hdc, COLOR_BACK);
				SetTextColor(hdc, COLOR_TEXT);
				u32 length = cstr_length(buffer);
				if (length > 0) {
					TextOutA(hdc, 0, 0, buffer, length);
				}
				EndPaint(hwnd, &ps);
			}
			return 0;
		case WM_KEYDOWN:
			switch (wParam) {
				case VK_LEFT:
				case VK_RIGHT:
				case VK_DOWN:
				case VK_UP:
				case VK_HOME:
				case VK_END:
				case VK_DELETE:
				case VK_BACK:
					break;
				case VK_ESCAPE:
					PostMessage(hwnd, WM_CLOSE, 0, 0);
					break;

			}
			return 0;
		case WM_LBUTTONDOWN:
			MessageBeep(MB_ICONINFORMATION);
			return 0;
		case WM_RBUTTONDOWN:
			MessageBeep(MB_ICONINFORMATION);
			return 0;
		case WM_CHAR:
			switch (wParam) {
				case VK_RETURN:
					break;
				case VK_TAB:
					break;
				case VK_BACK:
					break;
				default:
					if (wParam >= 32 && wParam != 127) {
						insert_character((char)wParam);
						InvalidateRect(hwnd, NULL, TRUE);
					}
			}
			break;
	}
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Getting Started


Motivation

In my last post, I described my frustration with modern software along with a desire to look back at the "good ol' days" of computing when software was simpler. Recently, simplicity in software is a guiding principal I've adopted, and that remains as one of the primary motivations for writing my own code editor. However, another important factor is that I really have no idea how these things work. There are some basic things that are obvious, but I have never entered the world of tool development or text editing, and I think having some hands-on experience would help me better apricate and understand software tools.

So I can summarize my motivations:

  • Need for a simple code editor
  • Need for a code editor that works on an air gapped Windows 7 machine
  • Need to free myself from as many dependencies as possible to remain in control of my own destiny
  • Need to learn something new

The motivations could also serve as goals for the project, and it's very likely that, by that measure, the project will fail. I will proceed, however. At the very least I will learn something new, which is my top priority. If I have to go back to using existing editors on Windows 11 it's not the end of the world, but I want to give this a try before accepting that fate.

The question I need to ask myself next is "how can I get this done?"

Software

Code Editor

If I don't already have a code editor, how can I write the code for the editor? I'm going to start with notepad.exe. This might sound crazy, but it also serves as a milestone marker. I know that I can start using my own code editor to develop itself once I have the required feature parity with notepad.

Compiler

It's obvious, but in order to compile code into a useable program I need a compiler. I already went through great pain trying and failing to install Build Tools for Visual Studio on my Windows 7 machine, because it is offline. Even after creating the offline installer and manually deploying certificates, the installer does not function and no actionable error messages are written to the logs. I will try GCC next. I've used GCC for some embedded programming in the past, but not for Windows development. It looks like installing GCC on a machine without internet access is not difficult, but who can be sure? My success or failure will be noted in the next post.

Hardware

For transparency's sake, I will include the actual hardware I am using for my dev machine:

HP EliteDesk 800 G2 Mini (refurbished)

  • Intel Core i5-6500T @ 2.5ghz
  • 16gb DDR4 RAM
  • 250gb HDD

The Plan

Before I start coding I think it's good to have a plan, even if the plan changes immediately and often. I know so little about how these things work that I expect this list to change dramatically. For now, these are the features I am looking at, roughly in order:

Prereq: get a compiler installed and working

  1. Win32 Window
    1. Message Loop
    2. Back Buffer + Blit
    3. Simple screen painting
  2. Font + Text Rendering
    1. Evaluate stb_truetype integration
    2. Render hardcoded text to screen
  3. Typing, Cursor and Gap Buffer
    1. Translate keystrokes to text
    2. Insert and delete text
    3. Blinking cursor
  4. File I/O
    1. Open, Close and Save files to disc
  5. Code Navigation
    1. Implement ability to search for, and jump to, text
    2. Show line numbers
  6. Hotkeys
    1. Copy, Cut, Paste
    2. Support common code-related commands
  7. Multi-pane
    1. Support having two files open at once
    2. Support having the same file open twice with independent cursors
  8. External Process
    1. Support running a BAT from a hotkey to compile code
    2. Support debugger integration (jump to line)
  9. Color Themes
    1. Support syntax highlighting
    2. Support custom colors

Something I want to note is that I don't have a plan to add mouse support. I have always lived in a world where a mouse was available to navigate code and I think it's always held me back. I want to get to a point where my hands never leave the keyboard, but I lack the discipline to do it myself. This could be an opportunity to force myself to forego the mouse by simply not supporting it.

Once this post goes live I will begin to put my plan into action.

Looking Back


The Past

My very first home computer was a Windows 95 machine which crashed frequently (although this could have been a hardware issue all along?). I upgraded to Windows ME which made things worse, but I was young and had time. I ended up learning a lot to play Diablo 2 without losing my hardcore character to lag or crashes. I can still vividly remember the horror of hearing "Looking for Baal" as the screen stayed black, only to finally load in with my character dead on the ground. The internet was still in its infancy at the time; there was no AI, no Google. I just had to try stuff until it worked.

When I got to college I used Windows XP. The OS crashes were mostly gone. For my C++ course work, we were instructed to use an IDE called Quincy which was primitive by modern standards. It crashed all the time (but Windows XP didn't) and I learned the hard way that pressing CTRL+S every few seconds was essential. This is a habit I continue to this day, although most modern software autosaves. Even with all these problems, I don't ever remember feeling discouraged when it came to programming, or about computers in general. Maybe I was just a naive youth, but I think my enthusiasm endured because I was witnessing computer hardware and software make continual improvements every day.

The Present

Now it's 2025. I've been using Windows 11 and Visual Studio for a long time, on both professional and personal projects. I find myself frequently feeling discouraged whenever I sit down to program. Time is now far more scarce than when I was young and every minute that I waste with OS updates, incompatibilities, and slowness feels excruciating. It's frustrating and makes me want to quit. Modern software has become exceptionally over complicated and it sucks! Even opening a folder takes forever on a modern gaming system! At least I'm not alone in feeling this way.

To mitigate this, I started using tools that were built by people who actually care about time. I started using 10x Editor and RemedyBG to make coding tolerable again. I use File Pilot to avoid the feeling of walking through tar with explorer.exe. But somehow, Windows 11 keeps getting worse and I keep noticing.

I can still remember the days before things were so broken and complicated! So, instead of looking forward I started looking back.

"If you want to set off and go develop some grand new thing, you don't need millions of dollars of capitalization. You need enough pizza and Diet Coke to stick in your refrigerator, a cheap PC to work on, and the dedication to go through with it." - John Carmack

Inspired by the numinous John Carmack quote above, I bought a cheap PC to serve as my primary programming machine and put Windows 10 LTSC on it. I briefly tried to install Linux but none of my attempts were successful. Some distros wouldn't even install on the hardware I was using. The ones that did install wouldn't boot. I actually blame Microsoft and Intel for this since I think many of the problems involve things like "TPM" or "UEFI" or whatever other complex BIOS junk that exists today. I admit that I do not have a deep understanding of chipset/BIOS minutia, and I have no interest in learning. I want to program, I don't want to be a systems admin.

I used that machine for about a year, and things were mostly good. However, LTSC still has an expiration date, and Windows 10 is still not fun. After a particularly depressing session, I thought hard about the last time I had a good experience with a computer. It was back when Windows 7 was in its prime. Windows 7 smoothed over many of the problems Vista introduced and mostly just got out of my way. It didn't have the modern Windows telemetry nightmare. It was stable. Software mostly worked. So I decided to try an experiment and give Windows 7 another try.

Immediately I discovered my machine wouldn't work because it was too new. The chipset is not supported by Windows 7 drivers. So I bought something old instead. The 10+ year old PC I bought has no wireless network adapter, which is excellent for me since putting this device online might destroy it. The software I use seems to still have support for Windows 7, even if unofficially. Unfortunately, 10x Editor does not render correctly which leaves me heartbroken.

The Future

So what can I do? I need an editor that respects me and my time, which narrows it down to very few. I used to use 4Coder, but it's long since been discontinued. There is a community version, so I gave that shot. Unfortunately, installing Build Tools for Visual Studio on an offline machine wouldn't work for me, even when using the fancy command line flags to create an offline installer. This leaves me concerned since I typically use Microsoft's compiler. I will try GCC to see if I have better luck, but for now I have no code editor and no compiler.

If I think about what I need in an editor, it's actually very little:

  • manipulate text
  • copy and paste
  • support keyboard shortcuts
  • launch a .bat to compile my code when I press CTRL+B

Syntax highlighting might be nice but isn't required. I just want to type, compile, and run programs.

The Focus editor sounds great, but doesn't run on Windows 7. I'm not cool enough or smart enough to learn emacs. Notepad is actually pretty close, but doesn't fulfil my short-list of needs.

I think I'll just write my own.