z_mirror/hare-kilo

git clone https://git.raiguard.me/z_mirror/hare-kilo

Caleb HeuerGet compiling on latest Harecaef1ef

trunk
9.3 KiB418 linesraw
1use ascii;
2use bufio;
3use encoding::utf8;
4use fs;
5use fmt;
6use io;
7use memio;
8use os;
9use strings;
10use unix::tty;
11
12def CONTROLS: str = "^S: save  ^Q: quit";
13def SCRATCH: str = "*scratch*";
14def TABSTOP: u8 = 8;
15
16// Escape codes reference: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
17def EC_ALTBUF_HIDE: str = "\x1b[?1049l";
18def EC_ALTBUF_SHOW: str = "\x1b[?1049h";
19def EC_CURSOR_HIDE: str = "\x1b[?25l";
20def EC_CURSOR_HOME: str = "\x1b[H";
21def EC_CURSOR_SHOW: str = "\x1b[?25h";
22def EC_ERASE_LINE: str = "\x1b[K";
23def EC_STYLE_DEFAULT: str = "\x1b[m";
24def EC_STYLE_REVERSE: str = "\x1b[7m";
25
26type key = enum u32 {
27	SPACE = ' ',
28	ESCAPE = '\x1b',
29	MOD_CTRL = '\x1f',
30	DELETE = '\x7f',
31	// Unicode private use area
32	ARROW_DOWN = '\uFF00',
33	ARROW_LEFT,
34	ARROW_RIGHT,
35	ARROW_UP,
36	END,
37	HOME,
38	PAGE_DOWN,
39	PAGE_UP,
40};
41
42type editor = struct {
43	// TODO: This uses a ton of memory because runes are encoded as u32
44	buf: [][]rune,
45	bufname: (str | void),
46	cursor_x: size,
47	cursor_y: size,
48	dirty: bool,
49	msg: str,
50	offset_x: size,
51	offset_y: size,
52	screen_cols: size,
53	screen_rows: size,
54	termios: tty::termios,
55};
56
57// TERMINAL
58
59fn terminal_read_key() u32 = {
60	let buf: [4]u8 = [0...];
61	const read = io::read(os::stdin_file, &buf)! as size;
62	if (read == 0)
63		fmt::fatal("Failed to read from stdin");
64	const first = buf[0];
65	switch (first) {
66	case '\x1b' =>
67		switch (buf[1]) {
68		case '[' =>
69			switch (buf[2]) {
70			case '5' =>
71				if (buf[3] == '~')
72					return key::PAGE_UP;
73			case '6' =>
74				if (buf[3] == '~')
75					return key::PAGE_DOWN;
76			case 'A' =>
77				return key::ARROW_UP;
78			case 'B' =>
79				return key::ARROW_DOWN;
80			case 'C' =>
81				return key::ARROW_RIGHT;
82			case 'D' =>
83				return key::ARROW_LEFT;
84			case 'F' =>
85				return key::END;
86			case 'H' =>
87				return key::HOME;
88			case => void;
89			};
90		case => void;
91		};
92		return '\x1b';
93	case => void;
94	};
95	return first: u32;
96};
97
98// ROW OPERATIONS
99
100fn editor_delete(e: *editor) void = {
101	if (e.cursor_x > 0) {
102		delete(e.buf[e.cursor_y][e.cursor_x - 1]);
103		e.cursor_x -= 1;
104	} else if (e.cursor_y > 0) {
105		let previous_line = &e.buf[e.cursor_y - 1];
106		const previous_line_len = len(previous_line);
107		append(previous_line, e.buf[e.cursor_y]...)!;
108		delete(e.buf[e.cursor_y]);
109		e.cursor_y -= 1;
110		e.cursor_x = previous_line_len;
111	};
112	e.dirty = true;
113};
114
115fn editor_insert(e: *editor, k: rune) void = {
116	insert(e.buf[e.cursor_y][e.cursor_x], k)!;
117	e.cursor_x += 1;
118	e.dirty = true;
119};
120
121fn editor_newline(e: *editor) void = {
122	let current_line = &e.buf[e.cursor_y];
123	const next_line_len = len(current_line) - e.cursor_x;
124	const next_line = if (next_line_len > 0) {
125		let next_line: []rune = alloc(['\0'...], next_line_len)!;
126		next_line[..] = current_line[e.cursor_x..];
127		delete(e.buf[e.cursor_y][e.cursor_x..]);
128		yield next_line;
129	} else {
130		yield []: []rune;
131	};
132	insert(e.buf[e.cursor_y + 1], next_line)!;
133	e.cursor_x = 0;
134	e.cursor_y += 1;
135	e.dirty = true;
136};
137
138// FILE OPERATIONS
139
140fn editor_open(e: *editor, path: str) void = {
141	editor_close(e);
142	let file = os::open(path, fs::flag::RDONLY)!;
143	defer io::close(file)!;
144	let buf: [][]rune = [];
145	let first: []rune = [];
146	append(buf, first)!;
147
148	let line = 0;
149	let new_row = false;
150	for (true) {
151		match (bufio::read_rune(file)) {
152		case utf8::invalid =>
153			fmt::fatal("File must be valid UTF-8");
154		case let err: io::error =>
155			fmt::fatal(io::strerror(err));
156		case let c: rune =>
157			if (new_row) {
158				new_row = false;
159				line += 1;
160				let next_line: []rune = [];
161				append(buf, next_line)!;
162			};
163			switch (c) {
164			case '\n' =>
165				new_row = true;
166			case =>
167				append(buf[line], c)!;
168			};
169		case io::EOF =>
170			break;
171		};
172	};
173	e.buf = buf;
174	e.bufname = path;
175	e.dirty = false;
176};
177
178fn editor_close(e: *editor) void = {
179	for (let i = 0z; i < len(e.buf); i += 1) {
180		free(e.buf[i]);
181	};
182	free(e.buf);
183};
184
185fn editor_save(e: *editor) void = {
186	if (e.bufname is void) {
187		e.msg = "Cannot save scratch buffer";
188		return;
189	};
190	let file = os::open(e.bufname as str, fs::flag::TRUNC | fs::flag::WRONLY)!;
191	defer io::close(file)!;
192
193	for (let y = 0z; y < len(e.buf); y += 1) {
194		const line = &e.buf[y];
195		// We can't use the spread operator here :(
196		for (let x = 0z; x < len(line); x += 1)
197			fmt::fprint(file, line[x])!;
198		fmt::fprint(file, '\n')!;
199	};
200
201	e.msg = "Saved to disk";
202	e.dirty = false;
203};
204
205// OUTPUT
206
207fn editor_scroll(e: *editor) void = {
208	if (e.cursor_x > len(e.buf[e.cursor_y]))
209		e.cursor_x = len(e.buf[e.cursor_y]);
210	if (e.cursor_x < e.offset_x)
211		e.offset_x = e.cursor_x;
212	if (e.cursor_x >= e.offset_x + e.screen_cols - 1)
213		e.offset_x = e.cursor_x - e.screen_cols + 1;
214	if (e.cursor_y < e.offset_y)
215		e.offset_y = e.cursor_y;
216	if (e.cursor_y >= e.offset_y + e.screen_rows - 1)
217		e.offset_y = e.cursor_y - e.screen_rows + 2;
218};
219
220fn editor_draw_line(e: *editor, out: *memio::stream, line: *[]rune) void = {
221	if (e.offset_x > len(line)) return;
222	let length = len(line) - e.offset_x;
223	if (length > e.screen_cols)
224		length = e.screen_cols;
225	const end = e.offset_x + length;
226	let dx: size = 0;
227	for (let x = e.offset_x; x < end; x += 1) {
228		const c = line[x];
229		switch (c) {
230		case '\t' =>
231			for (let i = 0u8; i < TABSTOP; i += 1) {
232				fmt::fprint(out, ' ')!;
233				dx += 1;
234				if (dx > e.screen_cols) break;
235			};
236		case =>
237			fmt::fprint(out, c)!;
238			dx += 1;
239		};
240		if (dx > e.screen_cols) break;
241	};
242	for (let x = dx; x < e.screen_cols; x += 1)
243		fmt::fprint(out, key::SPACE: u32)!;
244};
245
246fn editor_draw_screen(e: *editor) void = {
247	// Update offset to keep cursor on the screen
248	editor_scroll(e);
249	// Construct contents
250	let out = memio::dynamic();
251	defer io::close(&out)!;
252	fmt::fprintf(&out, "{}{}", EC_CURSOR_HOME, EC_CURSOR_HIDE)!;
253	// Buffer lines
254	for (let y = 0z; y < e.screen_rows - 1; y += 1) {
255		const disp_y = e.offset_y + y;
256		if (disp_y >= len(e.buf)) {
257			fmt::fprint(&out, "~")!;
258			fmt::fprint(&out, EC_ERASE_LINE)!;
259		} else {
260			const line = e.buf[disp_y];
261			editor_draw_line(e, &out, &line);
262		};
263		fmt::fprint(&out, "\r\n")!;
264	};
265	// Status line
266	// FIXME: Avoid heap allocation
267	const state = fmt::asprintf(
268		"{}:{}:{}",
269		if (e.bufname is str) e.bufname as str else SCRATCH,
270		e.cursor_x + 1, e.cursor_y + 1,
271	)!;
272	const msg = e.msg;
273	let space_count = e.screen_cols - len(state) - len(msg) - len(CONTROLS) - 1;
274	if (e.dirty)
275		space_count -= 3;
276	const spaces: []u8 = alloc([' '...], space_count)!;
277	fmt::fprintf(&out, "{}{}",
278		EC_STYLE_REVERSE,
279		CONTROLS
280	)!;
281	io::write(&out, spaces)!;
282	fmt::fprintf(&out, "{} {}{}{}",
283		msg,
284		if (e.dirty) "[*]" else "",
285		state,
286		EC_STYLE_DEFAULT
287	)!;
288	free(state);
289	free(spaces);
290	// Show and position cursor
291	let cursor_x = {
292		const line = e.buf[e.cursor_y];
293		let tabs = 0z;
294		for (let x = e.offset_x; x < e.cursor_x; x += 1) {
295			if (line[x] == '\t')
296				tabs += 1;
297		};
298		yield e.cursor_x - tabs + (tabs * TABSTOP: size);
299	};
300	fmt::fprintf(
301		&out,
302		"\x1b[{};{}H",
303		e.cursor_y - e.offset_y + 1,
304		cursor_x + 1
305	)!;
306	fmt::fprint(&out, EC_CURSOR_SHOW)!;
307	// Commit to terminal
308	io::write(os::stdout_file, memio::buffer(&out))!;
309};
310
311// INPUT
312
313fn editor_move_cursor(e: *editor, k: u32) void = {
314	switch (k) {
315	case key::ARROW_DOWN =>
316		if (e.cursor_y < len(e.buf) - 1)
317			e.cursor_y += 1;
318	case key::ARROW_LEFT =>
319		if (e.cursor_x > 0) {
320			e.cursor_x -= 1;
321		} else if (e.cursor_y > 0) {
322			e.cursor_y -= 1;
323			e.cursor_x = len(e.buf[e.cursor_y]);
324		};
325	case key::ARROW_RIGHT =>
326		const line = e.buf[e.cursor_y];
327		if (e.cursor_x < len(line)) {
328			e.cursor_x += 1;
329		} else if (e.cursor_y < len(e.buf) - 1) {
330			e.cursor_y += 1;
331			e.cursor_x = 0;
332		};
333	case key::ARROW_UP =>
334		if (e.cursor_y > 0)
335			e.cursor_y -= 1;
336	case => void;
337	};
338};
339
340// Returns `true` if the program should exit
341fn editor_process_key(e: *editor) bool = {
342	const k = terminal_read_key();
343	switch (k) {
344	case key::ARROW_DOWN, key::ARROW_LEFT, key::ARROW_RIGHT, key::ARROW_UP =>
345		editor_move_cursor(e, k);
346	case key::HOME =>
347		e.cursor_x = 0;
348	case key::END =>
349		e.cursor_x = len(e.buf[e.cursor_y]);
350	case key::PAGE_DOWN =>
351		e.cursor_y = e.offset_y + e.screen_rows - 2;
352		if (e.cursor_y > len(e.buf) - 1)
353			e.cursor_y = len(e.buf) - 1;
354		for (let i = 0z; i < e.screen_rows - 1; i += 1)
355			editor_move_cursor(e, key::ARROW_DOWN);
356	case key::PAGE_UP =>
357		e.cursor_y = e.offset_y;
358		for (let i = 0z; i < e.screen_rows - 1; i += 1)
359			editor_move_cursor(e, key::ARROW_UP);
360	case 'q' & key::MOD_CTRL =>
361		return true;
362	case 's' & key::MOD_CTRL =>
363		editor_save(e);
364	case '\b', key::DELETE =>
365		editor_delete(e);
366	case '\r' =>
367		editor_newline(e);
368	case =>
369		if (k == '\t' || ascii::isprint(k: rune))
370			editor_insert(e, k: rune);
371	};
372	return false;
373};
374
375// INIT
376
377fn editor_init() editor = {
378	let termios = tty::termios_query(os::stdin_file)!;
379	fmt::fprint(os::stdout_file, EC_ALTBUF_SHOW)!;
380	tty::makeraw(&termios)!;
381
382	let buf: [][]rune = [];
383	// Invariant: There is always at least one line in a buffer
384	append(buf, [])!;
385
386	const winsize = tty::winsize(os::stdout_file)!;
387
388	return editor {
389		buf = buf,
390		bufname = void,
391		screen_cols = winsize.columns: size,
392		screen_rows = winsize.rows: size,
393		termios = termios,
394		...
395	};
396};
397
398fn editor_finish(e: editor) void = {
399	editor_close(&e);
400	fmt::fprint(os::stdout_file, EC_ALTBUF_HIDE)!;
401	tty::termios_restore(&e.termios);
402
403};
404
405export fn main() void = {
406	let e = editor_init();
407	defer editor_finish(e);
408
409	if (len(os::args) > 1)
410		editor_open(&e, os::args[1]);
411
412	editor_draw_screen(&e);
413	for (true) {
414		e.msg = "";
415		if (editor_process_key(&e)) break;
416		editor_draw_screen(&e);
417	};
418};