Skip to content

Commit

Permalink
Merge pull request #211 from markersniffen/markersniffen
Browse files Browse the repository at this point in the history
sniffen post update
  • Loading branch information
Skytrias committed Jun 5, 2024
2 parents 56ba2d0 + 1d6dc6b commit 7cd1c86
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 6 deletions.
220 changes: 214 additions & 6 deletions content/news/2024-06.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ edit_multiline :: proc(..., cursor:^Txt_Pt, auto_scroll:^bool ...)
}
```

Although the code above is a simpler version of codin's `edit_multiline` widget, it accurately captures how I implemented the described features. The actual `edit_multiline` fuction definition looks like:
The above code above is a simpler version of codin's `edit_multiline` widget that actually exists, but it accurately captures how I implemented the described features. For what it's worth, the actual `edit_multiline` fuction definition looks like:

```
edit_multiline :: proc(
Expand All @@ -428,14 +428,222 @@ The inputs:

The return values:
- Box_Ops is a struct that contains any state affected by input, e.g. clicking, hovering, releasing, etc.
- []^Box is a temporary array of the visible boxes. This allows me to easily access the boxes after I run through main widget. For example, I use this returned array when highlighting rows that contain build errors.
- []int is an array of line indices that correspond to each of the boxes returned.
- []^Box and []int are temporary arrays of the visible boxes/line indices that I use to highlighting and jump to to build errors.

### Conclusion
### Build Errors

As implied by the return vaues above, I added the ability to run a build from within codin, so here's a breakdown of how that system works and integrates with the UI. I created the concept of a terminal, defined loosely by this struct:

```
Terminal :: struct {
panel: ^ui.Panel, // ui panel that the terminal is linked to
command: string, // a string to hold the build command (e.g. "odin run ./code --debug")
vpool: g.VPool, // pool to hold memory for build error strings
build_errors: [dynamic]Build_Error, // array of build error structs
error_list: map[string]g.Simple_List(Build_Error), // map of errors by string (file name)
active_error: ^Build_Error, // currently highlighted error
build_status: Build_Status, // enum {Nil, Failure, Success}
build_thread: ^thread.Thread, // build thread
}
```

A `Build_Error` looks like this:

```
Build_Error :: struct {
prev,next: ^Build_Error, // pointers to create linked list
path: string, // path to file that error is in
file: ^File, // pointer to file object that error is in
message: string, // error message
point: ui.Text_Pt, // row & column where error occurs
}
```

I create a new panel and add an `edit_text` widget to fill out the terminal.command string:

<video class="ratio ratio-16x9 mb-1 rounded" controls src="static\images/news/2024-06-codin_command-string.mp4"></video>

I make a call via the win32 api to start a process using the previously defined command:

```
execute_terminal_command :: proc(process_command: string) {
p := &app.terminal.process
p.security_attributes.nLength = size_of(windows.SECURITY_ATTRIBUTES)
p.security_attributes.bInheritHandle = true
p.security_attributes.lpSecurityDescriptor = nil
// create pipe
if !windows.CreatePipe(&p.child_out_read, &p.child_out_write, &p.security_attributes, 0) { ui.print("CreatePipe failed!", windows.GetLastError()) }
if !windows.CreatePipe(&p.child_in_read, &p.child_in_write, &p.security_attributes, 0) { ui.print("CreatePipe failed!", windows.GetLastError()) }
What I've described is just a snippet of what goes on beneath the hood of codin. Some parts are built into the UI library (panel splitting, string allocation, text parsing) while and others are specific to the codin (build commands, build error highlighting, searching).
if !windows.SetHandleInformation(p.child_out_read, windows.HANDLE_FLAG_INHERIT, 0) { ui.print("Stdout SetHandleInformation") }
if !windows.SetHandleInformation(p.child_in_write, windows.HANDLE_FLAG_INHERIT, 0) { ui.print("Stdout SetHandleInformation") }
si: windows.STARTUPINFOW
si.cb = size_of(si)
si.hStdError = p.child_out_write
si.hStdOutput = p.child_out_write
si.hStdInput = p.child_in_read
si.dwFlags += windows.STARTF_USESTDHANDLES
// Start the child process.
pcommand := windows.utf8_to_utf16(process_command)
pdir := windows.utf8_to_utf16(app.project.path)
if(!windows.CreateProcessW(nil, &pcommand[0], nil, nil, true, windows.CREATE_SUSPENDED|windows.CREATE_NO_WINDOW, nil, &pdir[0], &si, &p.process_info)) {
ui.print("CreateProcess failed!", windows.GetLastError())
return
}
windows.ResumeThread(p.process_info.hThread);
windows.CloseHandle( p.child_out_write )
windows.CloseHandle( p.child_in_read )
if app.terminal.build_thread != nil do thread.terminate(app.terminal.build_thread, 0)
app.terminal.build_thread = thread.create_and_start_with_data(&app, terminal_read)
}
```

...and then start a new thread to read from the pipe and fill out an array of Build Errors.

```
terminal_read :: proc(data:rawptr) {
a := (^App)(data)
t := &a.terminal
p := &a.terminal.process
<video class="ratio ratio-16x9 mb-1 rounded" controls src="/images/news/2024-06-codin_end.mp4"></video>
BUFSIZE :: 4096
read: windows.DWORD
written: windows.DWORD
buf: [BUFSIZE]byte
success: bool
g.vp_clear(&t.vpool) // clear memory/array/list for each build
clear(&t.build_errors)
clear(&t.error_list)
// read from pipe
for {
success = bool(windows.ReadFile(p.child_out_read, &buf, BUFSIZE, &read, nil))
if !success || read == 0 do break
chunk_buffer := g.vp_alloc(&t.vpool, int(read))
copy(chunk_buffer, buf[:])
chunk := string(chunk_buffer)
start: int
// split into lines
for letter, li in chunk {
if letter == '\n' {
// parse errors into Build_Errors
parse_build_error(t, chunk[start:li])
start = li + 1
}
}
}
if len(t.build_errors) == 0 {
t.build_status = .Success
t.active_error = nil
return
}
t.build_status = .Failure
set_active_error(&t.build_errors[0])
}
```

Back on the UI side, there are two jobs to do - first is to draw a list of build errors. In the new terminal panel, I add a `scrollbox` widget and then draw each error inside of clickable buttons:
```
terminal_panel :: proc() {
panel_begin()
size(.PARENT, 1, .CHILDREN, 1)
scrollbox("errors")
{
pi: int
// iterate through map of file paths
for path in terminal.error_list {
ei: int
// advance through linked list of errors
for e := terminal.e_list[path].first; e != nil; e = e.next {
axis(.X)
size(.PARENT, 1, .TEXT, 1)
error_box := create_box({"error button", pi, ei}, { .FOCUS, .CLICKABLE, .HOT_EFFECTS, .ACTIVE_EFFECTS })
// if error box clicked, set active error:
process_ops(error_box)
if error_box.ops.clicked do set_active_error(e)
// highlight button if active error
if e == terminal.active_error do error_box.flags += {.DRAW_BG, .DRAW_BORDER, .DRAW_GRADIENT }
push_parent(error_box)
// display an icon, row/col numbers, error filepath, and the error message itself:
size(.TEXT, 1, .TEXT, 1)
icon(ICON_CIRCLE, "###", ei, pi)
set_font(.Mono)
size(.PARENT, .1, .PARENT, 1)
text("Row ", e.point.row, " Col ", e.point.col, "###", ei, pi)
size(.PARENT, .2, .PARENT, 1)
text(e.path, "###", ei, pi)
size(.MIN_SIBLINGS, .1, .PARENT, 1)
text(e.message, "###", ei, pi)
pop()
ei += 1
}
pi += 1
}
}
panel_end()
}
```

<video class="ratio ratio-16x9 mb-1 rounded" controls src="images/news/2024-06-codin_build-errors1.mp4"></video>

The last step is to look back at the `edit_multiline` widget and highlight any visible rows that contain build errors. The full widget returns a list of visible boxes and row indices, so I used those like this:

```
buffer_panel :: proc() {
begin_panel()
text_buffer := get_buffer_by_panel(get_ctx_panel())
// draw text buffer via the edit_multiline widget
ops, visible_boxes, visible_rows := edit_multiline(...)
// get error list by the buffer's file's path:
error_list, lok := app.terminal.error_list[text_buffer.file.path]
// for each visible row:
for row, ri in visible_rows {
// for each build error in linked list
for e := error_list.first; e != nil; e = e.next {
// if this visible row matches an error's row...
if row == e.point.row {
// get the visible box
box := visible_boxes[ri]
if terminal.active_error == e {
// highlight visible box one color if it has the selected error
box.border_color = get_color(.ERROR)
} else {
// and a less harsh color if has a non selected error
box.border_color = get_color(.WARNING)
}
}
}
}
end_panel()
}
```

<video class="ratio ratio-16x9 mb-1 rounded" controls src="images/news/2024-06-codin_build-errors2.mp4"></video>

### Conclusion

What I've described is just a snippet of what goes on beneath the hood of codin. Hopefully that shed some light on my process and how a few aspects of both codin and my UI library work.

---

Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 7cd1c86

Please sign in to comment.