OXYGENHUB
# 2980b7b6d7cd enhanced-x16-emulator / src / source_view.c
5.5 KiB · C · last changed chore(): initial commit 26 days ago source blame history raw
9120e874 Andy McCall 1
// Commander X16 Emulator — source file discovery + caching for the debugger's
2
// native "Source" window. See source_view.h.
3
4
#include "source_view.h"
5
6
#include <stdio.h>
7
#include <stdlib.h>
8
#include <string.h>
9
10
// ─── Search paths ───────────────────────────────────────────────────
11
12
#define MAX_SEARCH_PATHS 32
13
static char *search_paths[MAX_SEARCH_PATHS];
14
static int   search_path_count = 0;
15
16
void source_view_add_path(const char *dir)
17
{
18
	if (!dir || !dir[0])
19
		return;
20
	// Already known: promote it rather than ignoring it. The insertion below
21
	// only means "most recently added is searched first" if re-adding a path
22
	// also counts as recent. Without this, loading A then B then A again leaves
23
	// B's directory ahead of A's, and two modules naming the same relative
24
	// source file would both resolve to B's copy.
25
	for (int i = 0; i < search_path_count; i++) {
26
		if (strcmp(search_paths[i], dir) == 0) {
27
			char *existing = search_paths[i];
28
			for (int j = i; j > 0; j--)
29
				search_paths[j] = search_paths[j - 1];
30
			search_paths[0] = existing;
31
			return;
32
		}
33
	}
34
	if (search_path_count >= MAX_SEARCH_PATHS)
35
		return;
36
37
	char *copy = malloc(strlen(dir) + 1);
38
	if (!copy)
39
		return;
40
	strcpy(copy, dir);
41
42
	// Insert at the front so the most-recently-added path is searched first.
43
	for (int i = search_path_count; i > 0; i--)
44
		search_paths[i] = search_paths[i - 1];
45
	search_paths[0] = copy;
46
	search_path_count++;
47
}
48
49
// ─── File cache ─────────────────────────────────────────────────────
50
51
#define MAX_CACHED_FILES 8
52
static source_file_t cache[MAX_CACHED_FILES];
53
static int           cache_count = 0;
54
55
static const char *basename_ptr(const char *path)
56
{
57
	const char *last = path;
58
	for (const char *p = path; *p; p++) {
59
		if (*p == '/' || *p == '\\')
60
			last = p + 1;
61
	}
62
	return last;
63
}
64
65
// Read an entire file into a NUL-terminated heap buffer. Returns NULL on failure.
66
static char *read_whole_file(const char *path)
67
{
68
	FILE *f;
69
#ifdef _WIN32
70
	if (fopen_s(&f, path, "rb") != 0 || !f)
71
		return NULL;
72
#else
73
	f = fopen(path, "rb");
74
	if (!f)
75
		return NULL;
76
#endif
77
	if (fseek(f, 0, SEEK_END) != 0) {
78
		fclose(f);
79
		return NULL;
80
	}
81
	long size = ftell(f);
82
	if (size < 0) {
83
		fclose(f);
84
		return NULL;
85
	}
86
	rewind(f);
87
88
	char *buf = malloc((size_t)size + 1);
89
	if (!buf) {
90
		fclose(f);
91
		return NULL;
92
	}
93
	size_t got = fread(buf, 1, (size_t)size, f);
94
	fclose(f);
95
	buf[got] = '\0';
96
	return buf;
97
}
98
99
// Try to open 'name' at 'dir/basename'. Returns a heap buffer or NULL.
100
static char *try_read_in_dir(const char *dir, const char *base)
101
{
102
	size_t need = strlen(dir) + 1 + strlen(base) + 1;
103
	char *path = malloc(need);
104
	if (!path)
105
		return NULL;
106
	snprintf(path, need, "%s/%s", dir, base);
107
	char *buf = read_whole_file(path);
108
	free(path);
109
	return buf;
110
}
111
112
// Split a NUL-terminated buffer into lines in place (rewrites \r\n / \n to NULs).
113
// Fills entry->lines / entry->count.
114
static void split_lines(source_file_t *entry)
115
{
116
	// First pass: count lines.
117
	int lines = 1;
118
	for (char *p = entry->text; *p; p++) {
119
		if (*p == '\n')
120
			lines++;
121
	}
122
123
	entry->lines = malloc((size_t)lines * sizeof(char *));
124
	if (!entry->lines) {
125
		entry->count = 0;
126
		return;
127
	}
128
129
	int idx = 0;
130
	char *start = entry->text;
131
	for (char *p = entry->text; ; p++) {
132
		if (*p == '\n' || *p == '\0') {
133
			// Trim a trailing CR for CRLF files.
134
			char *nl = p;
135
			if (nl > start && nl[-1] == '\r')
136
				nl[-1] = '\0';
137
			char terminator = *p;
138
			*p = '\0';
139
			if (idx < lines)
140
				entry->lines[idx++] = start;
141
			if (terminator == '\0')
142
				break;
143
			start = p + 1;
144
		}
145
	}
146
	entry->count = idx;
147
}
148
149
static void free_entry(source_file_t *entry)
150
{
151
	free(entry->name);
152
	free(entry->text);
153
	free(entry->lines);
154
	entry->name = NULL;
155
	entry->text = NULL;
156
	entry->lines = NULL;
157
	entry->count = 0;
158
	entry->found = false;
159
}
160
161
// Locate and read a source file by the name stored in the .dbg.
162
static void load_entry(source_file_t *entry, const char *name)
163
{
164
	entry->name = malloc(strlen(name) + 1);
165
	if (entry->name)
166
		strcpy(entry->name, name);
167
	entry->text = NULL;
168
	entry->lines = NULL;
169
	entry->count = 0;
170
	entry->found = false;
171
172
	char *buf = NULL;
173
174
	// 1) The path stored in the .dbg, as-is.
175
	buf = read_whole_file(name);
176
177
	// 2) Each registered search root + the file's basename.
178
	if (!buf) {
179
		const char *base = basename_ptr(name);
180
		for (int i = 0; i < search_path_count && !buf; i++)
181
			buf = try_read_in_dir(search_paths[i], base);
182
		// 3) Current working directory (basename only).
183
		if (!buf)
184
			buf = read_whole_file(base);
185
	}
186
187
	if (buf) {
188
		entry->text = buf;
189
		entry->found = true;
190
		split_lines(entry);
191
	}
192
}
193
194
const source_file_t *source_view_get(const char *name)
195
{
196
	if (!name || !name[0])
197
		return NULL;
198
199
	// Already cached?
200
	for (int i = 0; i < cache_count; i++) {
201
		if (cache[i].name && strcmp(cache[i].name, name) == 0)
202
			return &cache[i];
203
	}
204
205
	// Evict the oldest entry if the cache is full (simple FIFO).
206
	if (cache_count >= MAX_CACHED_FILES) {
207
		free_entry(&cache[0]);
208
		for (int i = 1; i < cache_count; i++)
209
			cache[i - 1] = cache[i];
210
		cache_count--;
211
	}
212
213
	source_file_t *entry = &cache[cache_count++];
214
	memset(entry, 0, sizeof(*entry));
215
	load_entry(entry, name);
216
	return entry;
217
}
218
219
void source_view_invalidate(void)
220
{
221
	for (int i = 0; i < cache_count; i++)
222
		free_entry(&cache[i]);
223
	cache_count = 0;
224
}
225
226
void source_view_free(void)
227
{
228
	source_view_invalidate();
229
	for (int i = 0; i < search_path_count; i++)
230
		free(search_paths[i]);
231
	search_path_count = 0;
232
}