Initial commit: Odin editor client + Kotlin daemon prototype
Native SDL3 editor written in Odin with a piece-table buffer core, backed by a Kotlin/JVM daemon speaking newline-delimited JSON over localhost TCP for Gradle import, Kotlin/Java diagnostics, and heuristic completion/hover/definition/references/rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
4a15350860
21 changed files with 8112 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Gradle / Kotlin build output
|
||||
daemon/.gradle/
|
||||
daemon/.kotlin/
|
||||
daemon/build/
|
||||
# Stray kotlinc output from ad-hoc compiles into the module root
|
||||
daemon/dev/
|
||||
daemon/META-INF/
|
||||
|
||||
# Compiled shaders
|
||||
client/odin/shaders/compiled/
|
||||
|
||||
# Odin build output
|
||||
/odin
|
||||
client/odin/odin
|
||||
|
||||
# Autopilot control file
|
||||
.opencode-autopilot-stop
|
||||
121
README.md
Normal file
121
README.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Native Kotlin Editor Prototype
|
||||
|
||||
This is the first slice of an Odin-native editor architecture backed by a small Kotlin/JVM daemon.
|
||||
|
||||
The boundary is intentionally simple: newline-delimited JSON over localhost TCP.
|
||||
|
||||
The native client tracks daemon request ids for interactive requests so concurrent popups and panels consume only their matching responses.
|
||||
|
||||
Text edits are debounced before sending `text/change` to the daemon; file switches, saves, navigation, and daemon-backed requests flush the active buffer immediately.
|
||||
|
||||
## Layout
|
||||
|
||||
- `client/odin/` - Odin client smoke test for the future native editor.
|
||||
- `daemon/` - Kotlin/JVM TCP daemon for Gradle/Kotlin intelligence.
|
||||
- `protocol.md` - current TCP message shape.
|
||||
|
||||
## Run
|
||||
|
||||
Start the daemon:
|
||||
|
||||
```sh
|
||||
gradle -p daemon run
|
||||
```
|
||||
|
||||
It prints a line like:
|
||||
|
||||
```text
|
||||
PORT 49321
|
||||
```
|
||||
|
||||
In another terminal, run the Odin client with that port:
|
||||
|
||||
```sh
|
||||
odin run client/odin -- 49321 /path/to/workspace
|
||||
```
|
||||
|
||||
The client sends `ping` and `workspace/open` requests and prints daemon responses.
|
||||
|
||||
Run the SDL editor window:
|
||||
|
||||
```sh
|
||||
odin run client/odin -- --sdl /path/to/workspace /path/to/file.kt
|
||||
```
|
||||
|
||||
The SDL mode starts the daemon automatically when no port is passed. You can still connect to an already-running daemon by passing its port before the file path:
|
||||
|
||||
Auto-started daemons are restarted automatically if the process exits or the TCP connection drops. Explicit-port mode does not restart external daemons.
|
||||
|
||||
```sh
|
||||
odin run client/odin -- --sdl /path/to/workspace <port> /path/to/file.kt
|
||||
```
|
||||
|
||||
The file path is optional. If no file is passed, the editor restores the last open files, active tab, and cursor positions for the workspace. If there is no saved state, it opens the current prototype default file under the workspace.
|
||||
|
||||
SDL mode shows a simple workspace file tree in the left sidebar. Click a file to open or focus it.
|
||||
|
||||
Open files appear in the editor tab strip; click a tab to switch active buffers or drag tabs to reorder them.
|
||||
|
||||
Use `Ctrl+W` to close the active buffer, or click a tab's `x`. Dirty buffers are not closed until saved.
|
||||
|
||||
SDL mode creates an SDL3 `GPUDevice` and uses a direct SDL3 GPU command-buffer renderer when a GPU backend is available. It batches editor rectangles, selections, cursor lines, panels, and `stb_truetype` font-atlas text into GPU vertices. It falls back to the regular SDL renderer in unsupported environments such as `SDL_VIDEODRIVER=dummy` smoke tests.
|
||||
|
||||
The GPU font loader checks `client/odin/assets/fonts/EditorMono.ttf`, `client/odin/assets/fonts/NotoSansMono-Regular.ttf`, then common system monospace font locations. The repo includes a bundled Noto Sans Mono font for the default path.
|
||||
|
||||
The GPU editor path includes lightweight Kotlin/Java syntax highlighting for comments, block comments, strings, character literals, triple-quoted Kotlin strings, keywords, numbers, and type-like identifiers.
|
||||
|
||||
Kotlin and Java diagnostics are shown in the gutter/status area with active-file error/warning/info counts, and as red dashed underlines in the GPU editor path. Kotlin diagnostics include Java source files from the same Gradle source roots for mixed-source projects, and unsaved Kotlin/Java buffers are compiled under their declared package path.
|
||||
|
||||
Use `F8` and `Shift+F8` to move to the next/previous diagnostic in the active file.
|
||||
|
||||
Use `Ctrl+Shift+M` to toggle the diagnostics panel. Use Up/Down, PageUp/PageDown, Home/End, mouse wheel, Enter, or row clicks to navigate issues.
|
||||
|
||||
Use `Ctrl+Shift+P` to open the command palette. Type to filter commands, use Up/Down, PageUp/PageDown, Home/End, mouse wheel, Enter, or row clicks to run actions. The palette includes Reload Workspace, which refreshes the file tree, reopens the Gradle workspace, clears stale diagnostics, and resyncs the active file.
|
||||
|
||||
Gradle workspace import records module source roots, test source roots, resource roots, classpath entries, tasks, and common generated source directories when they already exist under `build/generated/`. Diagnostics also include existing module output directories from `build/classes/` and `build/resources/` on their compiler classpath.
|
||||
|
||||
Use `Ctrl+Shift+T` to toggle Gradle tasks in the right sidebar. Use Up/Down, PageUp/PageDown, Home/End, mouse wheel, or row clicks to select tasks, and Enter to run the selected task; the sidebar shows the loaded task count and reports started/finished/failed events, the final response, and recent stdout/stderr output.
|
||||
|
||||
Compile starter SDL3 GPU shaders with:
|
||||
|
||||
```sh
|
||||
scripts/compile-shaders.sh
|
||||
```
|
||||
|
||||
The script uses `glslc` from shaderc and emits Vulkan/SPIR-V files under `client/odin/shaders/compiled/`. On Fedora, install it with `sudo dnf install glslc`.
|
||||
|
||||
Run autonomous opencode continuation loops with:
|
||||
|
||||
```sh
|
||||
scripts/autopilot-opencode.sh
|
||||
```
|
||||
|
||||
By default it runs `opencode run --dir "$PWD" --continue --dangerously-skip-permissions` repeatedly with an instruction to keep working through the task list and choose new tasks when the list is complete. Configure it with environment variables, for example:
|
||||
|
||||
```sh
|
||||
OPENCODE_SESSION=<session-id> AUTOPILOT_MAX_RUNS=5 scripts/autopilot-opencode.sh
|
||||
```
|
||||
|
||||
Create `.opencode-autopilot-stop` to stop the loop cleanly between iterations.
|
||||
|
||||
Set `AUTOPILOT_ALLOW_PERMISSIONS=0` to disable automatic permission approval.
|
||||
|
||||
The editor supports basic editing keys: arrows, `Ctrl+Left`/`Ctrl+Right` word movement, PageUp/PageDown, Home/End, Shift+movement selection, Backspace, Delete, Tab as four spaces, Shift+Tab outdent, auto-indent on Enter, mouse click/drag cursor selection, `Ctrl+C`/`Ctrl+X`/`Ctrl+V`, `Ctrl+F` find, `Ctrl+Z`/`Ctrl+Y` undo/redo, `Ctrl+S` save, and `Ctrl+Shift+S` save all. Replacing a selection with typed text, Enter, Tab, or a completion is one undo step, and undo/redo updates the dirty marker when returning to the saved version. Use `Ctrl+Shift+E` to toggle the Explorer sidebar. The editor and sidebars scroll independently based on mouse position.
|
||||
|
||||
Window size, sidebar widths, sidebar visibility, open files, active tab, and cursor positions are saved under the user cache directory and restored on startup.
|
||||
|
||||
In the find panel, Enter finds next and Shift+Enter finds previous. Visible matches are highlighted in the GPU editor path.
|
||||
|
||||
Press `Ctrl+M` in SDL GPU mode to toggle a font metrics overlay for debugging cursor/text alignment.
|
||||
|
||||
Press `Ctrl+Space` to open the completion popup. It sends `kotlin/completion` to the daemon and renders returned item labels with kind hints. Use Up/Down, PageUp/PageDown, Home/End, mouse wheel, Enter, or row clicks to choose and apply a completion by replacing the identifier prefix before the cursor. Completion returns Kotlin/Java keywords plus heuristic identifiers/declarations from the current buffer and workspace.
|
||||
|
||||
Press `Ctrl+H` to request hover information at the cursor. Hover describes language keywords and shows heuristic Kotlin/Java declaration lines/locations for identifiers when found in the current buffer or workspace.
|
||||
|
||||
Press `Ctrl+B` to go to definition. Definition currently uses a heuristic source scan for simple Kotlin/Java declarations and includes the current unsaved buffer.
|
||||
|
||||
Use `Alt+Left` and `Alt+Right` to navigate backward and forward through definition jumps.
|
||||
|
||||
Press `Ctrl+R` to find references for the identifier at the cursor. References currently use a heuristic source scan and are shown in a popup; use Up/Down, PageUp/PageDown, Home/End, mouse wheel, Enter, or row clicks to jump to a result.
|
||||
|
||||
Press `Ctrl+Shift+R` to open the rename preview panel. Enter a new name and press Enter to request preview edits. Rename remains preview-only and does not modify files yet.
|
||||
133
ROADMAP.md
Normal file
133
ROADMAP.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Native Kotlin Editor Roadmap
|
||||
|
||||
## Current State
|
||||
|
||||
- Odin editor core with piece-table storage.
|
||||
- File-backed editor buffers.
|
||||
- Cursor movement and line/column mapping.
|
||||
- SDL3 native window mode.
|
||||
- SDL3 GPU command-buffer renderer path when a GPU backend is available.
|
||||
- Basic text input, Enter with auto-indent, Backspace, Delete, Tab indent, arrows, Ctrl+Left/Ctrl+Right, selection, mouse click/drag cursor placement, and Ctrl+S save.
|
||||
- JVM daemon over TCP JSON-lines.
|
||||
- Gradle workspace import via Tooling API.
|
||||
- Kotlin diagnostics using compiler embeddable.
|
||||
- Async `diagnostics/publish` events rendered in the SDL gutter/status area and stored by file path for open buffers.
|
||||
|
||||
## Phase 1: Usable Editor
|
||||
|
||||
- Open arbitrary files in SDL mode.
|
||||
- Add project file tree. Done.
|
||||
- Support multiple open buffers.
|
||||
- Add proper viewport scrolling independent of cursor.
|
||||
- Add PageUp/PageDown/Home/End.
|
||||
- Add Ctrl+Left/Ctrl+Right word movement. Done.
|
||||
- Add mouse wheel scrolling.
|
||||
- Add click-to-move-cursor. Done.
|
||||
- Add mouse drag selection. Done.
|
||||
- Add undo/redo.
|
||||
- Add selection. Done.
|
||||
- Add copy/cut/paste. Done.
|
||||
- Add Delete key. Done.
|
||||
- Add Tab/indent/outdent. Done: Tab inserts four spaces and Shift+Tab outdents the current or selected lines.
|
||||
- Add auto-indent on Enter. Done.
|
||||
- Add search in file. Started: Ctrl+F find panel with next/previous navigation and visible match highlights.
|
||||
- Add Save All. Done.
|
||||
- Replace SDL debug text with proper font rendering. Started: direct GPU path uses an `stb_truetype` baked Noto Sans Mono atlas.
|
||||
- Replace renderer-backed drawing with a direct SDL3 GPU command-buffer quad/text pipeline. Started: rectangles, lines, panels, selection, cursor, and temporary text render through SDL3 GPU with SDL renderer fallback.
|
||||
- Add shader compilation pipeline. Started: GLSL sources plus `scripts/compile-shaders.sh` using `glslc` to emit SPIR-V.
|
||||
- Add syntax highlighting. Started: lightweight visible-line Kotlin/Java tokenizer in the GPU renderer.
|
||||
|
||||
## Phase 2: Daemon Integration
|
||||
|
||||
- Start JVM daemon automatically from Odin. Done.
|
||||
- Read daemon `PORT` from stdout. Done.
|
||||
- Stop daemon on editor exit. Done.
|
||||
- Restart daemon if it crashes. Done for auto-started SDL daemons; explicit external-port mode reports disconnects without spawning a replacement.
|
||||
- Replace ad-hoc JSON parsing in Odin. Done.
|
||||
- Track request IDs and pending responses. Done.
|
||||
- Distinguish responses and events cleanly. Done.
|
||||
- Store diagnostics per file, not only active file. Done: diagnostics events are queued and applied to the matching open buffer path.
|
||||
- Clear stale diagnostics on workspace reload.
|
||||
- Render diagnostic underlines/squiggles. Started: GPU renderer draws red dashed underlines on diagnostic lines.
|
||||
- Add next/previous diagnostic navigation. Done: F8 and Shift+F8 navigate active-file diagnostics.
|
||||
- Add diagnostics panel. Done: Ctrl+Shift+M opens active-file diagnostics with keyboard navigation.
|
||||
- Show Gradle tasks in UI. Started: Ctrl+Shift+T requests and displays tasks in a docked GPU right sidebar; responses are matched by request id.
|
||||
- Run selected Gradle task. Started: Enter in the Gradle tasks panel runs the selected task and reports success/failure.
|
||||
- Stream Gradle task output. Started: Gradle run lifecycle and stdout/stderr line events are surfaced in the tasks sidebar with recent output retained.
|
||||
- Add workspace reload/import action. Started: command palette Reload Workspace refreshes the file tree, sends `workspace/open`, clears stale diagnostics, and resyncs the active buffer.
|
||||
|
||||
## Phase 3: Kotlin And Java Intelligence
|
||||
|
||||
- Implement `kotlin/completion` in the daemon. Started: prefix-filtered Kotlin keywords plus heuristic current-buffer/workspace identifiers and declarations.
|
||||
- Add completion popup in SDL UI. Done: popup renders returned items and applies the selected completion with Enter or row click, using a single undoable replace edit.
|
||||
- Add keyboard navigation in completion popup. Done.
|
||||
- Implement `kotlin/hover`. Started: keyword hover plus heuristic declaration line/location lookup from the current buffer and workspace.
|
||||
- Render hover tooltip. Done.
|
||||
- Implement `kotlin/definition`. Started: heuristic simple declaration scan.
|
||||
- Open target file and jump to line/column. Done.
|
||||
- Add back/forward navigation stack. Done.
|
||||
- Add find references. Started: heuristic source scan with SDL references popup.
|
||||
- Add rename/refactor support. Started: daemon preview-only rename edits with SDL preview panel.
|
||||
- Add Java diagnostics/completion/navigation. Started: `.java` diagnostics use the JDK compiler with Gradle module classpath/sourcepath; completion/hover/definition/references scan Java source declarations heuristically.
|
||||
|
||||
## Phase 4: Backend Quality
|
||||
|
||||
- Replace compiler-per-request diagnostics with long-lived analysis state.
|
||||
- Cache Gradle module classpaths and source roots.
|
||||
- Move toward Kotlin Analysis API where feasible.
|
||||
- Correctly model multi-module projects. Started: diagnostics choose the module with the most specific matching source root.
|
||||
- Separate main/test source-set analysis. Started: diagnostics use main roots for main files and main+test roots for test files.
|
||||
- Include generated sources. Started: common existing `build/generated/` Kotlin/Java roots, including KSP/Kotlin Multiplatform source-set layouts, are added to Gradle module source roots.
|
||||
- Improve classpath and project dependency handling. Started: diagnostics include existing module output directories from `build/classes/` and `build/resources/`, including common JVM/KMP layouts; Kotlin diagnostics include Java sources from the matching Gradle source roots.
|
||||
|
||||
## Phase 5: Product Shape
|
||||
|
||||
- Add sidebar file tree.
|
||||
- Add editor tabs. Done: GPU tab strip shows open buffers and supports click-to-switch, close buttons, and drag reorder.
|
||||
- Add bottom panel for diagnostics/tasks/output.
|
||||
- Add command palette. Started: Ctrl+Shift+P opens a filterable GPU command palette for existing editor actions.
|
||||
- Add configurable keybindings.
|
||||
- Add theme settings.
|
||||
- Add font settings.
|
||||
- Persist last workspace, open files, cursor positions, and window layout. Started: window size, sidebars, open files, active tab, and cursor offsets are cached per workspace.
|
||||
|
||||
## Performance Work
|
||||
|
||||
- Incrementally update line index.
|
||||
- Compact piece table after many edits.
|
||||
- Cache rendered text/glyphs.
|
||||
- Avoid full-buffer `text/change` sync on every edit. Started: edit-triggered sync is debounced.
|
||||
- Debounce native-to-daemon text sync. Done: text edits schedule a delayed sync; file open/switch/navigation and daemon-backed commands still flush immediately.
|
||||
|
||||
## Next Work Queue
|
||||
|
||||
1. Open arbitrary file from SDL mode. Done.
|
||||
2. Add scrolling/PageUp/PageDown/Home/End. Done.
|
||||
3. Add undo/redo. Done.
|
||||
4. Add daemon auto-start. Done.
|
||||
5. Add robust protocol parser. Done.
|
||||
6. Add project file tree. Done.
|
||||
7. Add completion popup skeleton. Done.
|
||||
8. Wire completion responses into popup. Done.
|
||||
9. Implement real `kotlin/completion` in the daemon. Started: prefix-filtered Kotlin keywords plus heuristic current-buffer/workspace identifiers and declarations.
|
||||
10. Implement `kotlin/hover` and render hover tooltip. Started: keyword hover plus heuristic declaration line/location lookup from the current buffer and workspace.
|
||||
11. Implement `kotlin/definition` and Ctrl+B navigation. Started: heuristic simple declaration scan.
|
||||
12. Add back/forward navigation stack. Done.
|
||||
13. Add find references. Started: heuristic source scan with SDL references popup.
|
||||
14. Add rename/refactor support. Started: daemon preview-only rename edits with SDL preview panel.
|
||||
15. Add Ctrl+Left/Ctrl+Right word movement. Done.
|
||||
16. Add click-to-move-cursor. Done.
|
||||
17. Add Delete key. Done.
|
||||
18. Add Tab indentation and auto-indent. Done: Tab inserts four spaces, Shift+Tab outdents, and Enter preserves leading whitespace.
|
||||
19. Improve mouse support. Done: sidebar/editor wheel routing, sidebar scrolling, click-to-place-cursor, drag selection.
|
||||
20. Add selection. Done: mouse drag and Shift+movement selection with replacement on edit.
|
||||
|
||||
## Immediate Stabilization Goals
|
||||
|
||||
1. Text metrics correctness. In progress: GPU text uses explicit fixed-cell atlas placement, widened cell advance, GPU-aware hit-testing, and `Ctrl+M` metrics overlay.
|
||||
2. Font portability. Done for the prototype: bundled Noto Sans Mono is loaded from `client/odin/assets/fonts/` with system fallback paths.
|
||||
3. Proper font shaping baseline. Replace baked ASCII-only atlas with a more deliberate atlas/SDF pipeline.
|
||||
4. Renderer abstraction cleanup. Split UI drawing from SDL fallback/GPU backend details.
|
||||
5. Input consistency. Started: cursor, hit-testing, syntax spans, and selection use shared metrics in the GPU path; command palette, completion, references, diagnostics, and Gradle panels now keep keyboard selection visible, support row clicks, route wheel events when hovered, and handle PageUp/PageDown plus Home/End.
|
||||
6. Syntax quality. Started: visible-line tokenizer now carries `/* ... */` block comment and Kotlin triple-quoted string state across rendered lines, with Kotlin/Java keywords and character literals. Still needs semantic tokens.
|
||||
7. Editor essentials. Done for the current prototype: clipboard copy/cut/paste, Save All, richer Ctrl+F search, clickable GPU tabs, safe Ctrl+W tab close, tab close buttons, drag reorder, stale overlay cleanup after edits, dirty tracking by saved buffer version, robust open-buffer diagnostic matching, and single-step/no-op-aware undo for selection/full-buffer replacement are implemented.
|
||||
BIN
client/odin/assets/fonts/NotoSansMono-Regular.ttf
Normal file
BIN
client/odin/assets/fonts/NotoSansMono-Regular.ttf
Normal file
Binary file not shown.
538
client/odin/buffer.odin
Normal file
538
client/odin/buffer.odin
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
package main
|
||||
|
||||
Piece_Source :: enum {
|
||||
Original,
|
||||
Add,
|
||||
}
|
||||
|
||||
Piece :: struct {
|
||||
source: Piece_Source,
|
||||
start: int,
|
||||
len: int,
|
||||
}
|
||||
|
||||
Line_Index :: struct {
|
||||
starts: [dynamic]int,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
Buffer_Edit_Kind :: enum {
|
||||
Insert,
|
||||
Delete,
|
||||
Replace,
|
||||
}
|
||||
|
||||
Buffer_Edit :: struct {
|
||||
kind: Buffer_Edit_Kind,
|
||||
offset: int,
|
||||
text: [dynamic]u8,
|
||||
replacement: [dynamic]u8,
|
||||
}
|
||||
|
||||
Buffer :: struct {
|
||||
original: string,
|
||||
add: [dynamic]u8,
|
||||
pieces: [dynamic]Piece,
|
||||
undo: [dynamic]Buffer_Edit,
|
||||
redo: [dynamic]Buffer_Edit,
|
||||
|
||||
line_index: Line_Index,
|
||||
version: int,
|
||||
}
|
||||
|
||||
Cursor :: struct {
|
||||
offset: int,
|
||||
wanted_column: int,
|
||||
}
|
||||
|
||||
buffer_make :: proc(original: string) -> Buffer {
|
||||
buffer: Buffer
|
||||
buffer.original = original
|
||||
buffer.line_index.dirty = true
|
||||
|
||||
if len(original) > 0 {
|
||||
append(&buffer.pieces, Piece{.Original, 0, len(original)})
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
buffer_len :: proc(buffer: ^Buffer) -> int {
|
||||
total := 0
|
||||
for piece in buffer.pieces {
|
||||
total += piece.len
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
buffer_insert :: proc(buffer: ^Buffer, offset: int, text: string) {
|
||||
if len(text) == 0 do return
|
||||
|
||||
edit_text := clone_bytes(transmute([]u8)text)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Insert, offset = clamp_int(offset, 0, buffer_len(buffer)), text = edit_text})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_insert_raw(buffer, offset, text)
|
||||
}
|
||||
|
||||
buffer_insert_raw :: proc(buffer: ^Buffer, offset: int, text: string) {
|
||||
buffer_insert_raw_internal(buffer, offset, text, true)
|
||||
}
|
||||
|
||||
buffer_insert_raw_internal :: proc(buffer: ^Buffer, offset: int, text: string, bump_version: bool) {
|
||||
if len(text) == 0 do return
|
||||
|
||||
insert_at := clamp_int(offset, 0, buffer_len(buffer))
|
||||
add_start := len(buffer.add)
|
||||
text_bytes := transmute([]u8)text
|
||||
for b in text_bytes {
|
||||
append(&buffer.add, b)
|
||||
}
|
||||
|
||||
new_piece := Piece{.Add, add_start, len(text_bytes)}
|
||||
new_pieces: [dynamic]Piece
|
||||
inserted := false
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if !inserted && insert_at <= piece_end {
|
||||
inner := insert_at - cursor
|
||||
if inner > 0 {
|
||||
append(&new_pieces, Piece{piece.source, piece.start, inner})
|
||||
}
|
||||
append(&new_pieces, new_piece)
|
||||
if inner < piece.len {
|
||||
append(&new_pieces, Piece{piece.source, piece.start + inner, piece.len - inner})
|
||||
}
|
||||
inserted = true
|
||||
} else {
|
||||
append(&new_pieces, piece)
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
if !inserted {
|
||||
append(&new_pieces, new_piece)
|
||||
}
|
||||
|
||||
delete(buffer.pieces)
|
||||
buffer.pieces = new_pieces
|
||||
if bump_version do buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_delete_range :: proc(buffer: ^Buffer, offset: int, count: int) {
|
||||
if count <= 0 do return
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
if start == end do return
|
||||
|
||||
deleted := buffer_range_bytes(buffer, start, end - start)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Delete, offset = start, text = deleted})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_delete_range_raw(buffer, start, end - start)
|
||||
}
|
||||
|
||||
buffer_replace_range :: proc(buffer: ^Buffer, offset: int, count: int, replacement: string) {
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + max_int(count, 0), start, buffer_len(buffer))
|
||||
if start == end && len(replacement) == 0 do return
|
||||
|
||||
deleted := buffer_range_bytes(buffer, start, end - start)
|
||||
if bytes_equal(deleted[:], transmute([]u8)replacement) {
|
||||
delete(deleted)
|
||||
return
|
||||
}
|
||||
replacement_bytes := clone_bytes(transmute([]u8)replacement)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Replace, offset = start, text = deleted, replacement = replacement_bytes})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_delete_range_raw_internal(buffer, start, end - start, false)
|
||||
buffer_insert_raw_internal(buffer, start, replacement, false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_delete_range_raw :: proc(buffer: ^Buffer, offset: int, count: int) {
|
||||
buffer_delete_range_raw_internal(buffer, offset, count, true)
|
||||
}
|
||||
|
||||
buffer_delete_range_raw_internal :: proc(buffer: ^Buffer, offset: int, count: int, bump_version: bool) {
|
||||
if count <= 0 do return
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
if start == end do return
|
||||
|
||||
new_pieces: [dynamic]Piece
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_start := cursor
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if piece_end <= start || piece_start >= end {
|
||||
append(&new_pieces, piece)
|
||||
} else {
|
||||
keep_left := max_int(0, start - piece_start)
|
||||
keep_right := max_int(0, piece_end - end)
|
||||
|
||||
if keep_left > 0 {
|
||||
append(&new_pieces, Piece{piece.source, piece.start, keep_left})
|
||||
}
|
||||
if keep_right > 0 {
|
||||
right_start := piece.start + piece.len - keep_right
|
||||
append(&new_pieces, Piece{piece.source, right_start, keep_right})
|
||||
}
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
delete(buffer.pieces)
|
||||
buffer.pieces = new_pieces
|
||||
if bump_version do buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_undo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
edit, ok := buffer_pop_edit(&buffer.undo)
|
||||
if !ok do return false
|
||||
|
||||
switch edit.kind {
|
||||
case .Insert:
|
||||
buffer_delete_range_raw(buffer, edit.offset, len(edit.text))
|
||||
cursor.offset = edit.offset
|
||||
case .Delete:
|
||||
buffer_insert_raw(buffer, edit.offset, string(edit.text[:]))
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
case .Replace:
|
||||
buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.replacement), false)
|
||||
buffer_insert_raw_internal(buffer, edit.offset, string(edit.text[:]), false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
}
|
||||
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
buffer_push_edit(&buffer.redo, edit)
|
||||
return true
|
||||
}
|
||||
|
||||
buffer_redo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
edit, ok := buffer_pop_edit(&buffer.redo)
|
||||
if !ok do return false
|
||||
|
||||
switch edit.kind {
|
||||
case .Insert:
|
||||
buffer_insert_raw(buffer, edit.offset, string(edit.text[:]))
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
case .Delete:
|
||||
buffer_delete_range_raw(buffer, edit.offset, len(edit.text))
|
||||
cursor.offset = edit.offset
|
||||
case .Replace:
|
||||
buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.text), false)
|
||||
buffer_insert_raw_internal(buffer, edit.offset, string(edit.replacement[:]), false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
cursor.offset = edit.offset + len(edit.replacement)
|
||||
}
|
||||
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
buffer_push_edit(&buffer.undo, edit)
|
||||
return true
|
||||
}
|
||||
|
||||
buffer_bytes :: proc(buffer: ^Buffer) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
|
||||
for piece in buffer.pieces {
|
||||
switch piece.source {
|
||||
case .Original:
|
||||
chunk := buffer.original[piece.start:piece.start + piece.len]
|
||||
chunk_bytes := transmute([]u8)chunk
|
||||
for b in chunk_bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
case .Add:
|
||||
for b in buffer.add[piece.start:piece.start + piece.len] {
|
||||
append(&out, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
buffer_rebuild_line_index :: proc(buffer: ^Buffer) {
|
||||
delete(buffer.line_index.starts)
|
||||
buffer.line_index.starts = make([dynamic]int)
|
||||
append(&buffer.line_index.starts, 0)
|
||||
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
for b, i in text {
|
||||
if b == '\n' {
|
||||
append(&buffer.line_index.starts, i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.line_index.dirty = false
|
||||
}
|
||||
|
||||
buffer_line_count :: proc(buffer: ^Buffer) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
return len(buffer.line_index.starts)
|
||||
}
|
||||
|
||||
buffer_line_start :: proc(buffer: ^Buffer, line: int) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
if len(buffer.line_index.starts) == 0 do return 0
|
||||
index := clamp_int(line, 0, len(buffer.line_index.starts) - 1)
|
||||
return buffer.line_index.starts[index]
|
||||
}
|
||||
|
||||
buffer_line_end :: proc(buffer: ^Buffer, line: int) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
line_count := len(buffer.line_index.starts)
|
||||
if line_count == 0 do return 0
|
||||
|
||||
index := clamp_int(line, 0, line_count - 1)
|
||||
if index + 1 < line_count {
|
||||
return max_int(buffer.line_index.starts[index], buffer.line_index.starts[index + 1] - 1)
|
||||
}
|
||||
|
||||
return buffer_len(buffer)
|
||||
}
|
||||
|
||||
buffer_offset_to_line_col :: proc(buffer: ^Buffer, offset: int) -> (line: int, column: int) {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
target := clamp_int(offset, 0, buffer_len(buffer))
|
||||
result := 0
|
||||
for start, i in buffer.line_index.starts {
|
||||
if start > target do break
|
||||
result = i
|
||||
}
|
||||
|
||||
return result, target - buffer.line_index.starts[result]
|
||||
}
|
||||
|
||||
buffer_line_col_to_offset :: proc(buffer: ^Buffer, line: int, column: int) -> int {
|
||||
start := buffer_line_start(buffer, line)
|
||||
end := buffer_line_end(buffer, line)
|
||||
return clamp_int(start + max_int(column, 0), start, end)
|
||||
}
|
||||
|
||||
buffer_line_bytes :: proc(buffer: ^Buffer, line: int) -> [dynamic]u8 {
|
||||
start := buffer_line_start(buffer, line)
|
||||
end := buffer_line_end(buffer, line)
|
||||
return buffer_range_bytes(buffer, start, end - start)
|
||||
}
|
||||
|
||||
buffer_range_bytes :: proc(buffer: ^Buffer, offset: int, count: int) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
if count <= 0 do return out
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_start := cursor
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if piece_end > start && piece_start < end {
|
||||
local_start := max_int(start - piece_start, 0)
|
||||
local_end := piece.len - max_int(piece_end - end, 0)
|
||||
|
||||
switch piece.source {
|
||||
case .Original:
|
||||
chunk := buffer.original[piece.start + local_start:piece.start + local_end]
|
||||
chunk_bytes := transmute([]u8)chunk
|
||||
for b in chunk_bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
case .Add:
|
||||
for b in buffer.add[piece.start + local_start:piece.start + local_end] {
|
||||
append(&out, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
cursor_insert :: proc(buffer: ^Buffer, cursor: ^Cursor, text: string) {
|
||||
buffer_insert(buffer, cursor.offset, text)
|
||||
cursor.offset += len(text)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_backspace :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
if cursor.offset == 0 do return
|
||||
buffer_delete_range(buffer, cursor.offset - 1, 1)
|
||||
cursor.offset -= 1
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_delete_forward :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
if cursor.offset >= buffer_len(buffer) do return false
|
||||
buffer_delete_range(buffer, cursor.offset, 1)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
return true
|
||||
}
|
||||
|
||||
cursor_insert_newline_auto_indent :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
line, _ := buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
line_bytes := buffer_line_bytes(buffer, line)
|
||||
defer delete(line_bytes)
|
||||
|
||||
indent_len := 0
|
||||
for b in line_bytes {
|
||||
if b == ' ' || b == '\t' {
|
||||
indent_len += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
text: [dynamic]u8
|
||||
defer delete(text)
|
||||
append(&text, '\n')
|
||||
for b in line_bytes[:indent_len] {
|
||||
append(&text, b)
|
||||
}
|
||||
cursor_insert(buffer, cursor, string(text[:]))
|
||||
}
|
||||
|
||||
cursor_move_word_left :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
offset := clamp_int(cursor.offset, 0, len(text))
|
||||
for offset > 0 && is_space_byte(text[offset - 1]) {
|
||||
offset -= 1
|
||||
}
|
||||
if offset > 0 && is_identifier_byte(text[offset - 1]) {
|
||||
for offset > 0 && is_identifier_byte(text[offset - 1]) {
|
||||
offset -= 1
|
||||
}
|
||||
} else if offset > 0 {
|
||||
offset -= 1
|
||||
}
|
||||
|
||||
cursor.offset = offset
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_word_right :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
offset := clamp_int(cursor.offset, 0, len(text))
|
||||
if offset < len(text) && is_identifier_byte(text[offset]) {
|
||||
for offset < len(text) && is_identifier_byte(text[offset]) {
|
||||
offset += 1
|
||||
}
|
||||
} else if offset < len(text) {
|
||||
offset += 1
|
||||
}
|
||||
for offset < len(text) && is_space_byte(text[offset]) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
cursor.offset = offset
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_to_line_col :: proc(buffer: ^Buffer, cursor: ^Cursor, line: int, column: int) {
|
||||
cursor.offset = buffer_line_col_to_offset(buffer, line, column)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_vertical :: proc(buffer: ^Buffer, cursor: ^Cursor, delta: int) {
|
||||
line, _ := buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
target_line := clamp_int(line + delta, 0, buffer_line_count(buffer) - 1)
|
||||
cursor.offset = buffer_line_col_to_offset(buffer, target_line, cursor.wanted_column)
|
||||
}
|
||||
|
||||
is_identifier_byte :: proc(b: u8) -> bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
|
||||
}
|
||||
|
||||
is_space_byte :: proc(b: u8) -> bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
|
||||
buffer_destroy :: proc(buffer: ^Buffer) {
|
||||
buffer_clear_edits(&buffer.undo)
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
delete(buffer.add)
|
||||
delete(buffer.pieces)
|
||||
delete(buffer.line_index.starts)
|
||||
}
|
||||
|
||||
buffer_push_edit :: proc(stack: ^[dynamic]Buffer_Edit, edit: Buffer_Edit) {
|
||||
append(stack, edit)
|
||||
}
|
||||
|
||||
buffer_pop_edit :: proc(stack: ^[dynamic]Buffer_Edit) -> (edit: Buffer_Edit, ok: bool) {
|
||||
if len(stack^) == 0 do return {}, false
|
||||
|
||||
index := len(stack^) - 1
|
||||
edit = stack^[index]
|
||||
resize(stack, index)
|
||||
return edit, true
|
||||
}
|
||||
|
||||
buffer_clear_edits :: proc(stack: ^[dynamic]Buffer_Edit) {
|
||||
for edit in stack^ {
|
||||
delete(edit.text)
|
||||
delete(edit.replacement)
|
||||
}
|
||||
clear(stack)
|
||||
}
|
||||
|
||||
clone_bytes :: proc(bytes: []u8) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
for b in bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
bytes_equal :: proc(a, b: []u8) -> bool {
|
||||
if len(a) != len(b) do return false
|
||||
for value, index in a {
|
||||
if value != b[index] do return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
clamp_int :: proc(value, low, high: int) -> int {
|
||||
if value < low do return low
|
||||
if value > high do return high
|
||||
return value
|
||||
}
|
||||
|
||||
max_int :: proc(a, b: int) -> int {
|
||||
if a > b do return a
|
||||
return b
|
||||
}
|
||||
615
client/odin/daemon_client.odin
Normal file
615
client/odin/daemon_client.odin
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:net"
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
import "core:sync"
|
||||
import "core:thread"
|
||||
import "core:time"
|
||||
import json "core:encoding/json"
|
||||
|
||||
Protocol_Message_Kind :: enum {
|
||||
Invalid,
|
||||
Response,
|
||||
Event,
|
||||
}
|
||||
|
||||
Protocol_Message :: struct {
|
||||
kind: Protocol_Message_Kind,
|
||||
id: int,
|
||||
ok: bool,
|
||||
event: string,
|
||||
}
|
||||
|
||||
Daemon_Response :: struct {
|
||||
id: int,
|
||||
line: [dynamic]u8,
|
||||
}
|
||||
|
||||
Daemon_Event :: struct {
|
||||
line: [dynamic]u8,
|
||||
}
|
||||
|
||||
Daemon_Process :: struct {
|
||||
process: os.Process,
|
||||
stdout: ^os.File,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
Daemon_Client :: struct {
|
||||
socket: net.TCP_Socket,
|
||||
connected: bool,
|
||||
next_id: int,
|
||||
reader: ^thread.Thread,
|
||||
event_mutex: sync.Mutex,
|
||||
response_mutex: sync.Mutex,
|
||||
diagnostic_events: [dynamic]Daemon_Event,
|
||||
gradle_events: [dynamic]Daemon_Event,
|
||||
pending_response_ids: [dynamic]int,
|
||||
responses: [dynamic]Daemon_Response,
|
||||
}
|
||||
|
||||
daemon_connect :: proc(port: int) -> Daemon_Client {
|
||||
client := Daemon_Client{next_id = 1}
|
||||
if port <= 0 do return client
|
||||
|
||||
socket, err := net.dial_tcp("127.0.0.1", port)
|
||||
if err != nil {
|
||||
fmt.println("daemon connect failed:", err)
|
||||
return client
|
||||
}
|
||||
|
||||
client.socket = socket
|
||||
client.connected = true
|
||||
return client
|
||||
}
|
||||
|
||||
daemon_start_process :: proc(workspace: string) -> (Daemon_Process, int, bool) {
|
||||
child, ok := daemon_start_process_begin(workspace)
|
||||
if !ok {
|
||||
return child, 0, false
|
||||
}
|
||||
|
||||
port, port_ok := daemon_read_port(&child)
|
||||
if !port_ok {
|
||||
daemon_stop_process(&child)
|
||||
return child, 0, false
|
||||
}
|
||||
return child, port, true
|
||||
}
|
||||
|
||||
daemon_start_process_begin :: proc(workspace: string) -> (Daemon_Process, bool) {
|
||||
child := Daemon_Process{}
|
||||
|
||||
stdout_r, stdout_w, pipe_err := os.pipe()
|
||||
if pipe_err != nil {
|
||||
fmt.println("daemon pipe failed:", pipe_err)
|
||||
return child, false
|
||||
}
|
||||
|
||||
command := []string{"gradle", "-p", workspace, "run", "--quiet"}
|
||||
process, start_err := os.process_start(os.Process_Desc{
|
||||
command = command,
|
||||
stdout = stdout_w,
|
||||
})
|
||||
_ = os.close(stdout_w)
|
||||
|
||||
if start_err != nil {
|
||||
fmt.println("daemon start failed:", start_err)
|
||||
_ = os.close(stdout_r)
|
||||
return child, false
|
||||
}
|
||||
|
||||
child = Daemon_Process{process = process, stdout = stdout_r, running = true}
|
||||
return child, true
|
||||
}
|
||||
|
||||
daemon_stop_process :: proc(child: ^Daemon_Process) {
|
||||
if child.stdout != nil {
|
||||
_ = os.close(child.stdout)
|
||||
child.stdout = nil
|
||||
}
|
||||
if child.running {
|
||||
_ = os.process_terminate(child.process)
|
||||
state, wait_err := os.process_wait(child.process, 2 * time.Second)
|
||||
if wait_err != nil || !state.exited {
|
||||
_ = os.process_kill(child.process)
|
||||
_, _ = os.process_wait(child.process)
|
||||
}
|
||||
child.running = false
|
||||
}
|
||||
}
|
||||
|
||||
daemon_read_port :: proc(child: ^Daemon_Process) -> (int, bool) {
|
||||
line: [dynamic]u8
|
||||
defer delete(line)
|
||||
|
||||
start := time.now()
|
||||
buf: [1024]u8
|
||||
for time.diff(start, time.now()) < 30 * time.Second {
|
||||
state, wait_err := os.process_wait(child.process, 0)
|
||||
if wait_err == nil && state.exited {
|
||||
fmt.println("daemon exited before announcing port")
|
||||
return 0, false
|
||||
}
|
||||
|
||||
has_data, data_err := os.pipe_has_data(child.stdout)
|
||||
if data_err != nil {
|
||||
fmt.println("daemon stdout failed:", data_err)
|
||||
return 0, false
|
||||
}
|
||||
if !has_data {
|
||||
time.sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
n, read_err := os.read(child.stdout, buf[:])
|
||||
if read_err != nil {
|
||||
fmt.println("daemon stdout read failed:", read_err)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for b in buf[:n] {
|
||||
if b == '\n' {
|
||||
port, ok := daemon_parse_port_line(string(line[:]))
|
||||
if ok do return port, true
|
||||
clear(&line)
|
||||
} else {
|
||||
append(&line, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.println("daemon start timed out")
|
||||
return 0, false
|
||||
}
|
||||
|
||||
daemon_poll_port :: proc(child: ^Daemon_Process, line: ^[dynamic]u8) -> (int, bool, bool) {
|
||||
if child == nil || !child.running || child.stdout == nil do return 0, false, true
|
||||
|
||||
state, wait_err := os.process_wait(child.process, 0)
|
||||
if wait_err == nil && state.exited {
|
||||
fmt.println("daemon exited before announcing port")
|
||||
return 0, false, true
|
||||
}
|
||||
|
||||
has_data, data_err := os.pipe_has_data(child.stdout)
|
||||
if data_err != nil {
|
||||
fmt.println("daemon stdout failed:", data_err)
|
||||
return 0, false, true
|
||||
}
|
||||
if !has_data do return 0, false, false
|
||||
|
||||
buf: [1024]u8
|
||||
n, read_err := os.read(child.stdout, buf[:])
|
||||
if read_err != nil {
|
||||
fmt.println("daemon stdout read failed:", read_err)
|
||||
return 0, false, true
|
||||
}
|
||||
|
||||
for b in buf[:n] {
|
||||
if b == '\n' {
|
||||
port, ok := daemon_parse_port_line(string(line[:]))
|
||||
clear(line)
|
||||
if ok do return port, true, false
|
||||
} else {
|
||||
append(line, b)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false, false
|
||||
}
|
||||
|
||||
daemon_parse_port_line :: proc(line: string) -> (int, bool) {
|
||||
if !strings.has_prefix(line, "PORT ") do return 0, false
|
||||
port, ok := strconv.parse_int(line[len("PORT "):])
|
||||
if !ok do return 0, false
|
||||
return int(port), true
|
||||
}
|
||||
|
||||
daemon_start_reader :: proc(client: ^Daemon_Client) {
|
||||
if client.connected && client.reader == nil {
|
||||
client.reader = thread.create_and_start_with_data(rawptr(client), daemon_reader_thread)
|
||||
}
|
||||
}
|
||||
|
||||
daemon_close :: proc(client: ^Daemon_Client) {
|
||||
if client.connected {
|
||||
net.close(client.socket)
|
||||
client.connected = false
|
||||
}
|
||||
if client.reader != nil && thread.is_done(client.reader) {
|
||||
thread.destroy(client.reader)
|
||||
client.reader = nil
|
||||
}
|
||||
for &event in client.diagnostic_events {
|
||||
delete(event.line)
|
||||
}
|
||||
delete(client.diagnostic_events)
|
||||
client.diagnostic_events = nil
|
||||
for &event in client.gradle_events {
|
||||
delete(event.line)
|
||||
}
|
||||
delete(client.gradle_events)
|
||||
client.gradle_events = nil
|
||||
delete(client.pending_response_ids)
|
||||
client.pending_response_ids = nil
|
||||
for &response in client.responses {
|
||||
delete(response.line)
|
||||
}
|
||||
delete(client.responses)
|
||||
client.responses = nil
|
||||
}
|
||||
|
||||
daemon_send :: proc(client: ^Daemon_Client, message: string) {
|
||||
if !client.connected do return
|
||||
bytes := transmute([]byte)message
|
||||
_, err := net.send_tcp(client.socket, bytes)
|
||||
if err != nil {
|
||||
fmt.println("daemon send failed:", err)
|
||||
daemon_close(client)
|
||||
}
|
||||
}
|
||||
|
||||
daemon_track_response :: proc(client: ^Daemon_Client, id: int) {
|
||||
if id == 0 do return
|
||||
if sync.mutex_guard(&client.response_mutex) {
|
||||
append(&client.pending_response_ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
daemon_send_workspace_open :: proc(client: ^Daemon_Client, workspace: string) {
|
||||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%q}}}}\n", id, workspace)
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
daemon_send_text_open :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
||||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/open\",\"params\":{{\"path\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, text)
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
daemon_send_text_change :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
||||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/change\",\"params\":{{\"path\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, text)
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
daemon_send_completion :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_hover :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_definition :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_references :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/references\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_rename :: proc(client: ^Daemon_Client, path: string, line, column: int, new_name: string) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d,\"newName\":%q}}}}\n", id, path, line + 1, column + 1, new_name)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_gradle_tasks :: proc(client: ^Daemon_Client) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/tasks\",\"params\":{{}}}}\n", id)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_send_gradle_run :: proc(client: ^Daemon_Client, task: string) -> int {
|
||||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/run\",\"params\":{{\"task\":%q}}}}\n", id, task)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
}
|
||||
|
||||
daemon_sync_active_buffer :: proc(client: ^Daemon_Client, editor: ^Editor, open: bool) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
text := editor_active_text(editor)
|
||||
defer delete(text)
|
||||
|
||||
if open {
|
||||
daemon_send_text_open(client, active.path, active.buffer.version, string(text[:]))
|
||||
} else {
|
||||
daemon_send_text_change(client, active.path, active.buffer.version, string(text[:]))
|
||||
}
|
||||
}
|
||||
|
||||
daemon_reader_thread :: proc(data: rawptr) {
|
||||
client := (^Daemon_Client)(data)
|
||||
buf: [4096]u8
|
||||
line: [dynamic]u8
|
||||
defer delete(line)
|
||||
|
||||
for client.connected {
|
||||
n, err := net.recv_tcp(client.socket, buf[:])
|
||||
if err != nil || n == 0 {
|
||||
client.connected = false
|
||||
break
|
||||
}
|
||||
|
||||
for b in buf[:n] {
|
||||
if b == '\n' {
|
||||
daemon_store_line(client, string(line[:]))
|
||||
clear(&line)
|
||||
} else {
|
||||
append(&line, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
daemon_store_line :: proc(client: ^Daemon_Client, line: string) {
|
||||
message, value, ok := parse_protocol_message(line)
|
||||
if !ok do return
|
||||
defer json.destroy_value(value)
|
||||
if message.kind == .Response {
|
||||
if sync.mutex_guard(&client.response_mutex) {
|
||||
tracked := false
|
||||
for pending_id, index in client.pending_response_ids {
|
||||
if pending_id == message.id {
|
||||
ordered_remove(&client.pending_response_ids, index)
|
||||
tracked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tracked do return
|
||||
|
||||
response := Daemon_Response{id = message.id}
|
||||
for b in transmute([]u8)line {
|
||||
append(&response.line, b)
|
||||
}
|
||||
append(&client.responses, response)
|
||||
}
|
||||
return
|
||||
}
|
||||
if message.kind != .Event do return
|
||||
|
||||
if message.event == "gradle/run" {
|
||||
if sync.mutex_guard(&client.event_mutex) {
|
||||
event := Daemon_Event{}
|
||||
for b in transmute([]u8)line {
|
||||
append(&event.line, b)
|
||||
}
|
||||
append(&client.gradle_events, event)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if message.event != "diagnostics/publish" do return
|
||||
|
||||
if sync.mutex_guard(&client.event_mutex) {
|
||||
event := Daemon_Event{}
|
||||
for b in transmute([]u8)line {
|
||||
append(&event.line, b)
|
||||
}
|
||||
append(&client.diagnostic_events, event)
|
||||
}
|
||||
}
|
||||
|
||||
daemon_take_response :: proc(client: ^Daemon_Client, expected_id: int) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
if expected_id == 0 do return out
|
||||
|
||||
if sync.mutex_guard(&client.response_mutex) {
|
||||
for &response, index in client.responses {
|
||||
if response.id != expected_id do continue
|
||||
for b in response.line {
|
||||
append(&out, b)
|
||||
}
|
||||
delete(response.line)
|
||||
ordered_remove(&client.responses, index)
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
daemon_take_diagnostic_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
if !client.connected && len(client.diagnostic_events) == 0 do return out
|
||||
|
||||
if sync.mutex_guard(&client.event_mutex) {
|
||||
if len(client.diagnostic_events) == 0 do return out
|
||||
event := client.diagnostic_events[0]
|
||||
for b in event.line {
|
||||
append(&out, b)
|
||||
}
|
||||
delete(event.line)
|
||||
ordered_remove(&client.diagnostic_events, 0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
daemon_take_gradle_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
if !client.connected && len(client.gradle_events) == 0 do return out
|
||||
|
||||
if sync.mutex_guard(&client.event_mutex) {
|
||||
if len(client.gradle_events) == 0 do return out
|
||||
event := client.gradle_events[0]
|
||||
for b in event.line {
|
||||
append(&out, b)
|
||||
}
|
||||
delete(event.line)
|
||||
ordered_remove(&client.gradle_events, 0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
daemon_apply_latest_diagnostics :: proc(client: ^Daemon_Client, editor: ^Editor) {
|
||||
for {
|
||||
event := daemon_take_diagnostic_event(client)
|
||||
if len(event) == 0 {
|
||||
delete(event)
|
||||
return
|
||||
}
|
||||
|
||||
path, diagnostics := parse_diagnostics_event(string(event[:]))
|
||||
if len(path) == 0 || !editor_set_diagnostics_for_path(editor, path, diagnostics[:]) {
|
||||
diagnostics_destroy(diagnostics[:])
|
||||
}
|
||||
delete(path)
|
||||
delete(diagnostics)
|
||||
delete(event)
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics_destroy :: proc(diagnostics: []Diagnostic) {
|
||||
for diagnostic in diagnostics {
|
||||
delete(diagnostic.severity)
|
||||
delete(diagnostic.message)
|
||||
}
|
||||
}
|
||||
|
||||
parse_diagnostics_event :: proc(event: string) -> (string, [dynamic]Diagnostic) {
|
||||
diagnostics: [dynamic]Diagnostic
|
||||
|
||||
message, value, parsed := parse_protocol_message(event)
|
||||
if !parsed do return "", diagnostics
|
||||
defer json.destroy_value(value)
|
||||
if message.kind != .Event || message.event != "diagnostics/publish" do return "", diagnostics
|
||||
|
||||
params, has_params := json_object_get(value, "params")
|
||||
if !has_params do return "", diagnostics
|
||||
path, _ := json_get_string(params, "path")
|
||||
diagnostics_value, has_diagnostics := json_object_get(params, "diagnostics")
|
||||
if !has_diagnostics do return strings.clone(path), diagnostics
|
||||
|
||||
#partial switch items in diagnostics_value {
|
||||
case json.Array:
|
||||
for item in items {
|
||||
severity, _ := json_get_string(item, "severity")
|
||||
diagnostic_message, _ := json_get_string(item, "message")
|
||||
line, _ := json_get_int(item, "line")
|
||||
column, _ := json_get_int(item, "column")
|
||||
|
||||
append(&diagnostics, Diagnostic{
|
||||
line = max_int(line - 1, 0),
|
||||
column = max_int(column - 1, 0),
|
||||
severity = strings.clone(severity),
|
||||
message = strings.clone(diagnostic_message),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return strings.clone(path), diagnostics
|
||||
}
|
||||
|
||||
parse_protocol_message :: proc(line: string) -> (Protocol_Message, json.Value, bool) {
|
||||
value, err := json.parse_string(line, .JSON, true)
|
||||
if err != nil {
|
||||
return Protocol_Message{}, value, false
|
||||
}
|
||||
|
||||
message := Protocol_Message{}
|
||||
if event, ok := json_get_string(value, "event"); ok {
|
||||
message.kind = .Event
|
||||
message.event = event
|
||||
return message, value, true
|
||||
}
|
||||
|
||||
if id, ok := json_get_int(value, "id"); ok {
|
||||
message.kind = .Response
|
||||
message.id = id
|
||||
message.ok, _ = json_get_bool(value, "ok")
|
||||
return message, value, true
|
||||
}
|
||||
|
||||
return message, value, false
|
||||
}
|
||||
|
||||
json_object_get :: proc(value: json.Value, key: string) -> (json.Value, bool) {
|
||||
#partial switch object in value {
|
||||
case json.Object:
|
||||
if item, ok := object[key]; ok {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return json.Value{}, false
|
||||
}
|
||||
|
||||
json_get_string :: proc(value: json.Value, key: string) -> (string, bool) {
|
||||
item, ok := json_object_get(value, key)
|
||||
if !ok do return "", false
|
||||
#partial switch s in item {
|
||||
case json.String:
|
||||
return string(s), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
json_get_int :: proc(value: json.Value, key: string) -> (int, bool) {
|
||||
item, ok := json_object_get(value, key)
|
||||
if !ok do return 0, false
|
||||
#partial switch n in item {
|
||||
case json.Integer:
|
||||
return int(n), true
|
||||
case json.Float:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
json_get_bool :: proc(value: json.Value, key: string) -> (bool, bool) {
|
||||
item, ok := json_object_get(value, key)
|
||||
if !ok do return false, false
|
||||
#partial switch b in item {
|
||||
case json.Boolean:
|
||||
return bool(b), true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
449
client/odin/editor.odin
Normal file
449
client/odin/editor.odin
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
|
||||
Diagnostic :: struct {
|
||||
line: int,
|
||||
column: int,
|
||||
severity: string,
|
||||
message: string,
|
||||
}
|
||||
|
||||
Editor_Buffer :: struct {
|
||||
path: string,
|
||||
daemon_path: string,
|
||||
original_storage: []u8,
|
||||
buffer: Buffer,
|
||||
cursor: Cursor,
|
||||
dirty: bool,
|
||||
saved_version: int,
|
||||
selection_active: bool,
|
||||
selection_anchor: int,
|
||||
diagnostics: [dynamic]Diagnostic,
|
||||
}
|
||||
|
||||
Editor :: struct {
|
||||
buffers: [dynamic]Editor_Buffer,
|
||||
active: int,
|
||||
status: string,
|
||||
}
|
||||
|
||||
editor_open_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
data, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("open file failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer := Editor_Buffer{
|
||||
path = strings.clone(path),
|
||||
daemon_path = strings.clone(""),
|
||||
original_storage = data,
|
||||
buffer = buffer_make(string(data)),
|
||||
saved_version = 0,
|
||||
}
|
||||
|
||||
append(&editor.buffers, editor_buffer)
|
||||
editor.active = len(editor.buffers) - 1
|
||||
return true
|
||||
}
|
||||
|
||||
editor_open_or_focus_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
for &buffer, index in editor.buffers {
|
||||
if buffer.path == path {
|
||||
editor.active = index
|
||||
return true
|
||||
}
|
||||
}
|
||||
return editor_open_file(editor, path)
|
||||
}
|
||||
|
||||
editor_replace_active_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return false
|
||||
if active.path == path do return true
|
||||
|
||||
for &buffer, index in editor.buffers {
|
||||
if buffer.path == path {
|
||||
if index != editor.active {
|
||||
editor.buffers[editor.active], editor.buffers[index] = editor.buffers[index], editor.buffers[editor.active]
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if active.dirty {
|
||||
editor_set_status(editor, "Save current buffer before navigating to another file")
|
||||
return false
|
||||
}
|
||||
|
||||
data, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("open file failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer_destroy(active)
|
||||
active.path = strings.clone(path)
|
||||
active.daemon_path = strings.clone("")
|
||||
active.original_storage = data
|
||||
active.buffer = buffer_make(string(data))
|
||||
active.cursor = Cursor{}
|
||||
active.dirty = false
|
||||
active.saved_version = active.buffer.version
|
||||
active.selection_active = false
|
||||
active.selection_anchor = 0
|
||||
active.diagnostics = make([dynamic]Diagnostic)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_active_buffer :: proc(editor: ^Editor) -> ^Editor_Buffer {
|
||||
if len(editor.buffers) == 0 do return nil
|
||||
return &editor.buffers[editor.active]
|
||||
}
|
||||
|
||||
editor_replace_active_text :: proc(editor: ^Editor, text: string) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
buffer_replace_range(&active.buffer, 0, buffer_len(&active.buffer), text)
|
||||
active.cursor.offset = len(text)
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_active_text :: proc(editor: ^Editor) -> [dynamic]u8 {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil {
|
||||
empty: [dynamic]u8
|
||||
return empty
|
||||
}
|
||||
|
||||
return buffer_bytes(&active.buffer)
|
||||
}
|
||||
|
||||
editor_clear_selection :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
active.selection_active = false
|
||||
active.selection_anchor = active.cursor.offset
|
||||
}
|
||||
|
||||
editor_start_selection :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
if !active.selection_active {
|
||||
active.selection_active = true
|
||||
active.selection_anchor = active.cursor.offset
|
||||
}
|
||||
}
|
||||
|
||||
editor_selection_range :: proc(active: ^Editor_Buffer) -> (start: int, end: int, ok: bool) {
|
||||
if active == nil || !active.selection_active do return 0, 0, false
|
||||
start = min_int(active.selection_anchor, active.cursor.offset)
|
||||
end = max_int(active.selection_anchor, active.cursor.offset)
|
||||
return start, end, start < end
|
||||
}
|
||||
|
||||
editor_delete_selection :: proc(active: ^Editor_Buffer) -> bool {
|
||||
start, end, ok := editor_selection_range(active)
|
||||
if !ok do return false
|
||||
|
||||
buffer_delete_range(&active.buffer, start, end - start)
|
||||
active.cursor.offset = start
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_insert_text :: proc(active: ^Editor_Buffer, text: string) {
|
||||
if active == nil do return
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
if has_selection {
|
||||
buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, text)
|
||||
active.cursor.offset = selection_start + len(text)
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
return
|
||||
}
|
||||
cursor_insert(&active.buffer, &active.cursor, text)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_update_dirty :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
active.dirty = active.buffer.version != active.saved_version
|
||||
}
|
||||
|
||||
editor_outdent :: proc(active: ^Editor_Buffer) -> bool {
|
||||
if active == nil do return false
|
||||
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
start_line, _ := buffer_offset_to_line_col(&active.buffer, selection_start if has_selection else active.cursor.offset)
|
||||
end_line, end_col := buffer_offset_to_line_col(&active.buffer, selection_end if has_selection else active.cursor.offset)
|
||||
if has_selection && end_col == 0 && end_line > start_line {
|
||||
end_line -= 1
|
||||
}
|
||||
|
||||
total_removed := 0
|
||||
cursor_removed_before := 0
|
||||
anchor_removed_before := 0
|
||||
cursor_offset := active.cursor.offset
|
||||
anchor_offset := active.selection_anchor
|
||||
|
||||
for line := end_line; line >= start_line; line -= 1 {
|
||||
line_start := buffer_line_start(&active.buffer, line)
|
||||
line_end := buffer_line_end(&active.buffer, line)
|
||||
remove_count := editor_line_outdent_count(&active.buffer, line_start, line_end)
|
||||
if remove_count == 0 do continue
|
||||
|
||||
buffer_delete_range(&active.buffer, line_start, remove_count)
|
||||
total_removed += remove_count
|
||||
if line_start < cursor_offset {
|
||||
cursor_removed_before += min_int(remove_count, cursor_offset - line_start)
|
||||
}
|
||||
if line_start < anchor_offset {
|
||||
anchor_removed_before += min_int(remove_count, anchor_offset - line_start)
|
||||
}
|
||||
}
|
||||
|
||||
if total_removed == 0 do return false
|
||||
|
||||
active.cursor.offset = clamp_int(cursor_offset - cursor_removed_before, 0, buffer_len(&active.buffer))
|
||||
if has_selection {
|
||||
active.selection_anchor = clamp_int(anchor_offset - anchor_removed_before, 0, buffer_len(&active.buffer))
|
||||
} else {
|
||||
editor_clear_selection(active)
|
||||
}
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_update_dirty(active)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_line_outdent_count :: proc(buffer: ^Buffer, line_start, line_end: int) -> int {
|
||||
if line_start >= line_end do return 0
|
||||
bytes := buffer_range_bytes(buffer, line_start, min_int(4, line_end - line_start))
|
||||
defer delete(bytes)
|
||||
|
||||
remove_count := 0
|
||||
for b in bytes {
|
||||
if b == ' ' && remove_count < 4 {
|
||||
remove_count += 1
|
||||
} else if b == '\t' && remove_count == 0 {
|
||||
return 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return remove_count
|
||||
}
|
||||
|
||||
editor_insert_newline_auto_indent :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
if has_selection {
|
||||
buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, "\n")
|
||||
active.cursor.offset = selection_start + 1
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
} else {
|
||||
cursor_insert_newline_auto_indent(&active.buffer, &active.cursor)
|
||||
}
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_save_active :: proc(editor: ^Editor) -> bool {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return false
|
||||
|
||||
text := buffer_bytes(&active.buffer)
|
||||
defer delete(text)
|
||||
|
||||
err := os.write_entire_file(active.path, text[:])
|
||||
if err != nil {
|
||||
fmt.println("save failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
active.saved_version = active.buffer.version
|
||||
active.dirty = false
|
||||
return true
|
||||
}
|
||||
|
||||
editor_save_all :: proc(editor: ^Editor) -> bool {
|
||||
ok := true
|
||||
for &buffer in editor.buffers {
|
||||
if !buffer.dirty do continue
|
||||
|
||||
text := buffer_bytes(&buffer.buffer)
|
||||
err := os.write_entire_file(buffer.path, text[:])
|
||||
delete(text)
|
||||
if err != nil {
|
||||
fmt.println("save failed:", buffer.path, err)
|
||||
ok = false
|
||||
} else {
|
||||
buffer.saved_version = buffer.buffer.version
|
||||
buffer.dirty = false
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
editor_close_active :: proc(editor: ^Editor) -> bool {
|
||||
return editor_close_buffer(editor, editor.active)
|
||||
}
|
||||
|
||||
editor_close_buffer :: proc(editor: ^Editor, index: int) -> bool {
|
||||
if len(editor.buffers) <= 1 {
|
||||
editor_set_status(editor, "Cannot close the last buffer")
|
||||
return false
|
||||
}
|
||||
if index < 0 || index >= len(editor.buffers) do return false
|
||||
|
||||
active := &editor.buffers[index]
|
||||
if active.dirty {
|
||||
editor_set_status(editor, "Buffer has unsaved changes; save before closing")
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer_destroy(active)
|
||||
ordered_remove(&editor.buffers, index)
|
||||
if editor.active > index {
|
||||
editor.active -= 1
|
||||
}
|
||||
editor.active = clamp_int(editor.active, 0, len(editor.buffers) - 1)
|
||||
editor_set_status(editor, "Closed buffer")
|
||||
return true
|
||||
}
|
||||
|
||||
editor_set_status :: proc(editor: ^Editor, message: string) {
|
||||
delete(editor.status)
|
||||
editor.status = strings.clone(message)
|
||||
}
|
||||
|
||||
editor_set_active_diagnostics :: proc(editor: ^Editor, diagnostics: []Diagnostic) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
editor_buffer_set_diagnostics(active, diagnostics)
|
||||
}
|
||||
|
||||
editor_set_diagnostics_for_path :: proc(editor: ^Editor, path: string, diagnostics: []Diagnostic) -> bool {
|
||||
for &buffer in editor.buffers {
|
||||
if buffer.path == path || buffer.daemon_path == path {
|
||||
editor_buffer_set_diagnostics(&buffer, diagnostics)
|
||||
if buffer.daemon_path != path {
|
||||
delete(buffer.daemon_path)
|
||||
buffer.daemon_path = strings.clone(path)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
_, target_name := os.split_path(path)
|
||||
match_index := -1
|
||||
match_count := 0
|
||||
for &buffer, index in editor.buffers {
|
||||
_, buffer_name := os.split_path(buffer.path)
|
||||
if buffer_name == target_name {
|
||||
match_index = index
|
||||
match_count += 1
|
||||
}
|
||||
}
|
||||
if match_count == 1 {
|
||||
editor_buffer_set_diagnostics(&editor.buffers[match_index], diagnostics)
|
||||
delete(editor.buffers[match_index].daemon_path)
|
||||
editor.buffers[match_index].daemon_path = strings.clone(path)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
editor_clear_all_diagnostics :: proc(editor: ^Editor) {
|
||||
for &buffer in editor.buffers {
|
||||
editor_buffer_set_diagnostics(&buffer, []Diagnostic{})
|
||||
}
|
||||
}
|
||||
|
||||
editor_buffer_set_diagnostics :: proc(buffer: ^Editor_Buffer, diagnostics: []Diagnostic) {
|
||||
if buffer == nil do return
|
||||
|
||||
for diagnostic in buffer.diagnostics {
|
||||
delete(diagnostic.severity)
|
||||
delete(diagnostic.message)
|
||||
}
|
||||
delete(buffer.diagnostics)
|
||||
buffer.diagnostics = make([dynamic]Diagnostic)
|
||||
for diagnostic in diagnostics {
|
||||
append(&buffer.diagnostics, diagnostic)
|
||||
}
|
||||
}
|
||||
|
||||
editor_diagnostic_on_line :: proc(editor: ^Editor, line: int) -> (diagnostic: Diagnostic, ok: bool) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return {}, false
|
||||
|
||||
for diagnostic in active.diagnostics {
|
||||
if diagnostic.line == line {
|
||||
return diagnostic, true
|
||||
}
|
||||
}
|
||||
|
||||
return {}, false
|
||||
}
|
||||
|
||||
editor_diagnostic_counts :: proc(buffer: ^Editor_Buffer) -> (errors, warnings, infos: int) {
|
||||
if buffer == nil do return 0, 0, 0
|
||||
|
||||
for diagnostic in buffer.diagnostics {
|
||||
switch diagnostic.severity {
|
||||
case "error":
|
||||
errors += 1
|
||||
case "warning":
|
||||
warnings += 1
|
||||
case:
|
||||
infos += 1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
editor_print_visible :: proc(editor: ^Editor, first_line, line_count: int) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
last_line := min_int(first_line + line_count, buffer_line_count(&active.buffer))
|
||||
for line := first_line; line < last_line; line += 1 {
|
||||
bytes := buffer_line_bytes(&active.buffer, line)
|
||||
fmt.printf("%4d | %s\n", line + 1, string(bytes[:]))
|
||||
delete(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
editor_destroy :: proc(editor: ^Editor) {
|
||||
for &editor_buffer in editor.buffers {
|
||||
editor_buffer_destroy(&editor_buffer)
|
||||
}
|
||||
delete(editor.buffers)
|
||||
delete(editor.status)
|
||||
}
|
||||
|
||||
editor_buffer_destroy :: proc(editor_buffer: ^Editor_Buffer) {
|
||||
delete(editor_buffer.path)
|
||||
delete(editor_buffer.daemon_path)
|
||||
buffer_destroy(&editor_buffer.buffer)
|
||||
delete(editor_buffer.original_storage)
|
||||
for diagnostic in editor_buffer.diagnostics {
|
||||
delete(diagnostic.severity)
|
||||
delete(diagnostic.message)
|
||||
}
|
||||
delete(editor_buffer.diagnostics)
|
||||
}
|
||||
|
||||
min_int :: proc(a, b: int) -> int {
|
||||
if a < b do return a
|
||||
return b
|
||||
}
|
||||
581
client/odin/gpu_renderer.odin
Normal file
581
client/odin/gpu_renderer.odin
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import stbtt "vendor:stb/truetype"
|
||||
import SDL "vendor:sdl3"
|
||||
|
||||
GPU_Vertex :: struct {
|
||||
pos: [2]f32,
|
||||
color: [4]f32,
|
||||
}
|
||||
|
||||
GPU_Text_Vertex :: struct {
|
||||
pos: [2]f32,
|
||||
uv: [2]f32,
|
||||
color: [4]f32,
|
||||
}
|
||||
|
||||
GPU_Uniforms :: struct {
|
||||
viewport: [2]f32,
|
||||
}
|
||||
|
||||
GPU_Renderer :: struct {
|
||||
available: bool,
|
||||
device: ^SDL.GPUDevice,
|
||||
window: ^SDL.Window,
|
||||
pipeline: ^SDL.GPUGraphicsPipeline,
|
||||
text_pipeline: ^SDL.GPUGraphicsPipeline,
|
||||
vertex_shader: ^SDL.GPUShader,
|
||||
fragment_shader: ^SDL.GPUShader,
|
||||
text_vertex_shader: ^SDL.GPUShader,
|
||||
text_fragment_shader: ^SDL.GPUShader,
|
||||
vertex_buffer: ^SDL.GPUBuffer,
|
||||
transfer_buffer: ^SDL.GPUTransferBuffer,
|
||||
text_vertex_buffer: ^SDL.GPUBuffer,
|
||||
text_transfer_buffer: ^SDL.GPUTransferBuffer,
|
||||
font_texture: ^SDL.GPUTexture,
|
||||
font_sampler: ^SDL.GPUSampler,
|
||||
font_chars: [95]stbtt.bakedchar,
|
||||
font_advance: f32,
|
||||
vertices: [dynamic]GPU_Vertex,
|
||||
text_vertices: [dynamic]GPU_Text_Vertex,
|
||||
max_vertices: int,
|
||||
max_text_vertices: int,
|
||||
width: int,
|
||||
height: int,
|
||||
}
|
||||
|
||||
GPU_MAX_VERTICES :: 240000
|
||||
GPU_MAX_TEXT_VERTICES :: 120000
|
||||
GPU_FONT_ATLAS_SIZE :: 512
|
||||
GPU_FONT_PIXEL_HEIGHT :: 15.0
|
||||
GPU_FONT_BASELINE_OFFSET :: 11.0
|
||||
GPU_FONT_CELL_PADDING :: 1.0
|
||||
GPU_FONT_PATHS := [?]string{
|
||||
"client/odin/assets/fonts/EditorMono.ttf",
|
||||
"client/odin/assets/fonts/NotoSansMono-Regular.ttf",
|
||||
"/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf",
|
||||
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
|
||||
}
|
||||
|
||||
gpu_renderer_make :: proc(window: ^SDL.Window) -> GPU_Renderer {
|
||||
gpu: GPU_Renderer
|
||||
gpu.window = window
|
||||
gpu.max_vertices = GPU_MAX_VERTICES
|
||||
gpu.max_text_vertices = GPU_MAX_TEXT_VERTICES
|
||||
gpu.width = SDL_WINDOW_WIDTH
|
||||
gpu.height = SDL_WINDOW_HEIGHT
|
||||
gpu.vertices = make([dynamic]GPU_Vertex, 0, gpu.max_vertices)
|
||||
gpu.text_vertices = make([dynamic]GPU_Text_Vertex, 0, gpu.max_text_vertices)
|
||||
|
||||
gpu.device = SDL.CreateGPUDevice(SDL.GPUShaderFormat{.SPIRV}, true, nil)
|
||||
if gpu.device == nil {
|
||||
fmt.println("SDL GPU device failed, falling back:", SDL.GetError())
|
||||
return gpu
|
||||
}
|
||||
|
||||
if !SDL.ClaimWindowForGPUDevice(gpu.device, window) {
|
||||
fmt.println("SDL GPU window claim failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
gpu.vertex_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/rect.vert.spv", .VERTEX)
|
||||
gpu.fragment_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/rect.frag.spv", .FRAGMENT)
|
||||
gpu.text_vertex_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/text.vert.spv", .VERTEX)
|
||||
gpu.text_fragment_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/text.frag.spv", .FRAGMENT)
|
||||
if gpu.vertex_shader == nil || gpu.fragment_shader == nil || gpu.text_vertex_shader == nil || gpu.text_fragment_shader == nil {
|
||||
fmt.println("SDL GPU shaders unavailable; run scripts/compile-shaders.sh")
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
color_format := SDL.GetGPUSwapchainTextureFormat(gpu.device, window)
|
||||
if color_format == .INVALID {
|
||||
fmt.println("SDL GPU swapchain format failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
vb_desc := SDL.GPUVertexBufferDescription{
|
||||
slot = 0,
|
||||
pitch = size_of(GPU_Vertex),
|
||||
input_rate = .VERTEX,
|
||||
instance_step_rate = 0,
|
||||
}
|
||||
attrs := [?]SDL.GPUVertexAttribute{
|
||||
{location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Vertex, pos))},
|
||||
{location = 1, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Vertex, color))},
|
||||
}
|
||||
blend := SDL.GPUColorTargetBlendState{
|
||||
src_color_blendfactor = .SRC_ALPHA,
|
||||
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
|
||||
color_blend_op = .ADD,
|
||||
src_alpha_blendfactor = .ONE,
|
||||
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
|
||||
alpha_blend_op = .ADD,
|
||||
color_write_mask = SDL.GPUColorComponentFlags{.R, .G, .B, .A},
|
||||
enable_blend = true,
|
||||
enable_color_write_mask = true,
|
||||
}
|
||||
color_target := SDL.GPUColorTargetDescription{format = color_format, blend_state = blend}
|
||||
pipeline_info := SDL.GPUGraphicsPipelineCreateInfo{
|
||||
vertex_shader = gpu.vertex_shader,
|
||||
fragment_shader = gpu.fragment_shader,
|
||||
vertex_input_state = {
|
||||
vertex_buffer_descriptions = &vb_desc,
|
||||
num_vertex_buffers = 1,
|
||||
vertex_attributes = &attrs[0],
|
||||
num_vertex_attributes = len(attrs),
|
||||
},
|
||||
primitive_type = .TRIANGLELIST,
|
||||
rasterizer_state = {fill_mode = .FILL, cull_mode = .NONE, front_face = .COUNTER_CLOCKWISE},
|
||||
multisample_state = {sample_count = ._1},
|
||||
depth_stencil_state = {},
|
||||
target_info = {
|
||||
color_target_descriptions = &color_target,
|
||||
num_color_targets = 1,
|
||||
has_depth_stencil_target = false,
|
||||
},
|
||||
}
|
||||
|
||||
gpu.pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, pipeline_info)
|
||||
if gpu.pipeline == nil {
|
||||
fmt.println("SDL GPU pipeline failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
text_vb_desc := SDL.GPUVertexBufferDescription{
|
||||
slot = 0,
|
||||
pitch = size_of(GPU_Text_Vertex),
|
||||
input_rate = .VERTEX,
|
||||
instance_step_rate = 0,
|
||||
}
|
||||
text_attrs := [?]SDL.GPUVertexAttribute{
|
||||
{location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, pos))},
|
||||
{location = 1, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, uv))},
|
||||
{location = 2, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Text_Vertex, color))},
|
||||
}
|
||||
text_pipeline_info := pipeline_info
|
||||
text_pipeline_info.vertex_shader = gpu.text_vertex_shader
|
||||
text_pipeline_info.fragment_shader = gpu.text_fragment_shader
|
||||
text_pipeline_info.vertex_input_state = {
|
||||
vertex_buffer_descriptions = &text_vb_desc,
|
||||
num_vertex_buffers = 1,
|
||||
vertex_attributes = &text_attrs[0],
|
||||
num_vertex_attributes = len(text_attrs),
|
||||
}
|
||||
gpu.text_pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, text_pipeline_info)
|
||||
if gpu.text_pipeline == nil {
|
||||
fmt.println("SDL GPU text pipeline failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
buffer_size := u32(gpu.max_vertices * size_of(GPU_Vertex))
|
||||
gpu.vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = buffer_size})
|
||||
gpu.transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = buffer_size})
|
||||
text_buffer_size := u32(gpu.max_text_vertices * size_of(GPU_Text_Vertex))
|
||||
gpu.text_vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = text_buffer_size})
|
||||
gpu.text_transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = text_buffer_size})
|
||||
if gpu.vertex_buffer == nil || gpu.transfer_buffer == nil || gpu.text_vertex_buffer == nil || gpu.text_transfer_buffer == nil {
|
||||
fmt.println("SDL GPU buffers failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
if !gpu_create_font_atlas(&gpu) {
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
gpu.available = true
|
||||
return gpu
|
||||
}
|
||||
|
||||
gpu_renderer_destroy :: proc(gpu: ^GPU_Renderer) {
|
||||
if gpu.device != nil {
|
||||
_ = SDL.WaitForGPUIdle(gpu.device)
|
||||
if gpu.window != nil {
|
||||
SDL.ReleaseWindowFromGPUDevice(gpu.device, gpu.window)
|
||||
}
|
||||
if gpu.transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.transfer_buffer)
|
||||
if gpu.text_transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer)
|
||||
if gpu.vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.vertex_buffer)
|
||||
if gpu.text_vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.text_vertex_buffer)
|
||||
if gpu.font_sampler != nil do SDL.ReleaseGPUSampler(gpu.device, gpu.font_sampler)
|
||||
if gpu.font_texture != nil do SDL.ReleaseGPUTexture(gpu.device, gpu.font_texture)
|
||||
if gpu.pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.pipeline)
|
||||
if gpu.text_pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.text_pipeline)
|
||||
if gpu.fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.fragment_shader)
|
||||
if gpu.vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.vertex_shader)
|
||||
if gpu.text_fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_fragment_shader)
|
||||
if gpu.text_vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_vertex_shader)
|
||||
SDL.DestroyGPUDevice(gpu.device)
|
||||
}
|
||||
delete(gpu.vertices)
|
||||
delete(gpu.text_vertices)
|
||||
gpu^ = {}
|
||||
}
|
||||
|
||||
gpu_create_shader :: proc(device: ^SDL.GPUDevice, path: string, stage: SDL.GPUShaderStage) -> ^SDL.GPUShader {
|
||||
bytes, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("read shader failed:", path, err)
|
||||
return nil
|
||||
}
|
||||
defer delete(bytes)
|
||||
|
||||
shader := SDL.CreateGPUShader(device, {
|
||||
code_size = len(bytes),
|
||||
code = raw_data(bytes),
|
||||
entrypoint = "main",
|
||||
format = {.SPIRV},
|
||||
stage = stage,
|
||||
num_uniform_buffers = 1 if stage == .VERTEX else 0,
|
||||
num_samplers = 1 if stage == .FRAGMENT && strings_has_suffix(path, "text.frag.spv") else 0,
|
||||
})
|
||||
if shader == nil {
|
||||
fmt.println("CreateGPUShader failed:", path, SDL.GetError())
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
gpu_begin :: proc(gpu: ^GPU_Renderer) {
|
||||
clear(&gpu.vertices)
|
||||
clear(&gpu.text_vertices)
|
||||
w, h: i32
|
||||
if SDL.GetWindowSize(gpu.window, &w, &h) {
|
||||
gpu.width = int(w)
|
||||
gpu.height = int(h)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_rect :: proc(gpu: ^GPU_Renderer, x, y, w, h: f32, r, g, b, a: u8) {
|
||||
if w <= 0 || h <= 0 do return
|
||||
c := color_f32(r, g, b, a)
|
||||
gpu_push_quad(gpu, x, y, x + w, y + h, c)
|
||||
}
|
||||
|
||||
gpu_line :: proc(gpu: ^GPU_Renderer, x1, y1, x2, y2: f32, r, g, b, a: u8) {
|
||||
if abs_f32(x2 - x1) < 1 {
|
||||
x := min_f32(x1, x2)
|
||||
y := min_f32(y1, y2)
|
||||
gpu_rect(gpu, x, y, 1, abs_f32(y2 - y1), r, g, b, a)
|
||||
} else if abs_f32(y2 - y1) < 1 {
|
||||
x := min_f32(x1, x2)
|
||||
y := min_f32(y1, y2)
|
||||
gpu_rect(gpu, x, y, abs_f32(x2 - x1), 1, r, g, b, a)
|
||||
} else {
|
||||
gpu_rect(gpu, x1, y1, max_f32(abs_f32(x2 - x1), 1), 1, r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, r, g, b, a: u8) {
|
||||
if len(text) == 0 do return
|
||||
xpos := round_f32(x)
|
||||
ypos := y + GPU_FONT_BASELINE_OFFSET
|
||||
color := color_f32(r, g, b, a)
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
xpos = x
|
||||
ypos += SDL_LINE_HEIGHT
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 {
|
||||
ch = '?'
|
||||
}
|
||||
if ch == ' ' {
|
||||
xpos += gpu.font_advance
|
||||
continue
|
||||
}
|
||||
glyph := gpu.font_chars[ch - 32]
|
||||
glyph_w := f32(glyph.x1 - glyph.x0)
|
||||
glyph_h := f32(glyph.y1 - glyph.y0)
|
||||
x0 := round_f32(xpos + glyph.xoff)
|
||||
y0 := round_f32(ypos + glyph.yoff)
|
||||
x1 := x0 + glyph_w
|
||||
y1 := y0 + glyph_h
|
||||
s0 := f32(glyph.x0) / GPU_FONT_ATLAS_SIZE
|
||||
t0 := f32(glyph.y0) / GPU_FONT_ATLAS_SIZE
|
||||
s1 := f32(glyph.x1) / GPU_FONT_ATLAS_SIZE
|
||||
t1 := f32(glyph.y1) / GPU_FONT_ATLAS_SIZE
|
||||
gpu_push_text_quad(gpu, x0, y0, x1, y1, s0, t0, s1, t1, color)
|
||||
xpos += gpu.font_advance
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text_limited :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, max_chars: int, r, g, b, a: u8) {
|
||||
if max_chars <= 0 do return
|
||||
if len(text) <= max_chars {
|
||||
gpu_text(gpu, x, y, text, r, g, b, a)
|
||||
} else if max_chars <= 3 {
|
||||
gpu_text(gpu, x, y, text[:max_chars], r, g, b, a)
|
||||
} else {
|
||||
clipped := fmt.tprintf("%s...", text[:max_chars - 3])
|
||||
gpu_text(gpu, x, y, clipped, r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text_width :: proc(text: string) -> int {
|
||||
if len(text) == 0 do return 0
|
||||
width: f32 = 0
|
||||
max_width: f32 = 0
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
if width > max_width do max_width = width
|
||||
width = 0
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 do ch = '?'
|
||||
width += gpu_global_font_advance(ch)
|
||||
}
|
||||
if width > max_width do max_width = width
|
||||
return int(max_width + 0.5)
|
||||
}
|
||||
|
||||
gpu_font_text_advance :: proc(gpu: ^GPU_Renderer, text: string) -> f32 {
|
||||
if len(text) == 0 do return 0
|
||||
|
||||
start_x: f32 = 0
|
||||
x: f32 = start_x
|
||||
y: f32 = GPU_FONT_BASELINE_OFFSET
|
||||
max_width: f32 = 0
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
if x - start_x > max_width do max_width = x - start_x
|
||||
x = start_x
|
||||
y += SDL_LINE_HEIGHT
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 do ch = '?'
|
||||
quad: stbtt.aligned_quad
|
||||
x += gpu.font_advance
|
||||
}
|
||||
if x - start_x > max_width do max_width = x - start_x
|
||||
return max_width
|
||||
}
|
||||
|
||||
gpu_font_text_width :: proc(gpu: ^GPU_Renderer, text: string) -> int {
|
||||
return int(gpu_font_text_advance(gpu, text) + 0.5)
|
||||
}
|
||||
|
||||
gpu_present :: proc(gpu: ^GPU_Renderer) {
|
||||
if !gpu.available do return
|
||||
|
||||
command := SDL.AcquireGPUCommandBuffer(gpu.device)
|
||||
if command == nil do return
|
||||
|
||||
texture: ^SDL.GPUTexture
|
||||
width, height: u32
|
||||
if !SDL.WaitAndAcquireGPUSwapchainTexture(command, gpu.window, &texture, &width, &height) || texture == nil {
|
||||
_ = SDL.CancelGPUCommandBuffer(command)
|
||||
return
|
||||
}
|
||||
|
||||
vertex_count := len(gpu.vertices)
|
||||
text_vertex_count := len(gpu.text_vertices)
|
||||
if vertex_count > 0 {
|
||||
size := vertex_count * size_of(GPU_Vertex)
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.transfer_buffer, true)
|
||||
if mapped != nil {
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(gpu.vertices[:]), size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, gpu.transfer_buffer)
|
||||
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.transfer_buffer}, {buffer = gpu.vertex_buffer, size = u32(size)}, true)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
}
|
||||
}
|
||||
if text_vertex_count > 0 {
|
||||
size := text_vertex_count * size_of(GPU_Text_Vertex)
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer, true)
|
||||
if mapped != nil {
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(gpu.text_vertices[:]), size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer)
|
||||
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.text_transfer_buffer}, {buffer = gpu.text_vertex_buffer, size = u32(size)}, true)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
}
|
||||
}
|
||||
|
||||
target := SDL.GPUColorTargetInfo{
|
||||
texture = texture,
|
||||
clear_color = SDL.FColor{15.0 / 255.0, 17.0 / 255.0, 22.0 / 255.0, 1},
|
||||
load_op = .CLEAR,
|
||||
store_op = .STORE,
|
||||
}
|
||||
pass := SDL.BeginGPURenderPass(command, &target, 1, nil)
|
||||
if vertex_count > 0 {
|
||||
uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}}
|
||||
SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms))
|
||||
SDL.BindGPUGraphicsPipeline(pass, gpu.pipeline)
|
||||
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.vertex_buffer}), 1)
|
||||
SDL.DrawGPUPrimitives(pass, u32(vertex_count), 1, 0, 0)
|
||||
}
|
||||
if text_vertex_count > 0 {
|
||||
uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}}
|
||||
binding := SDL.GPUTextureSamplerBinding{texture = gpu.font_texture, sampler = gpu.font_sampler}
|
||||
SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms))
|
||||
SDL.BindGPUGraphicsPipeline(pass, gpu.text_pipeline)
|
||||
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.text_vertex_buffer}), 1)
|
||||
SDL.BindGPUFragmentSamplers(pass, 0, &binding, 1)
|
||||
SDL.DrawGPUPrimitives(pass, u32(text_vertex_count), 1, 0, 0)
|
||||
}
|
||||
SDL.EndGPURenderPass(pass)
|
||||
_ = SDL.SubmitGPUCommandBuffer(command)
|
||||
}
|
||||
|
||||
gpu_create_font_atlas :: proc(gpu: ^GPU_Renderer) -> bool {
|
||||
font_bytes, font_path, ok := gpu_read_font_file()
|
||||
if !ok {
|
||||
fmt.println("read font failed: no configured monospace font found")
|
||||
return false
|
||||
}
|
||||
defer delete(font_bytes)
|
||||
|
||||
atlas_size := GPU_FONT_ATLAS_SIZE * GPU_FONT_ATLAS_SIZE
|
||||
atlas := make([]u8, atlas_size)
|
||||
defer delete(atlas)
|
||||
|
||||
baked := stbtt.BakeFontBitmap(raw_data(font_bytes), 0, GPU_FONT_PIXEL_HEIGHT, raw_data(atlas), GPU_FONT_ATLAS_SIZE, GPU_FONT_ATLAS_SIZE, 32, len(gpu.font_chars), &gpu.font_chars[0])
|
||||
if baked <= 0 {
|
||||
fmt.println("font bake failed:", font_path)
|
||||
return false
|
||||
}
|
||||
widest_advance: f32 = 0
|
||||
for char in gpu.font_chars {
|
||||
glyph_width := char.xoff + f32(char.x1 - char.x0)
|
||||
widest_advance = max_f32(widest_advance, max_f32(char.xadvance, glyph_width))
|
||||
}
|
||||
gpu.font_advance = max_f32(ceil_f32(widest_advance + GPU_FONT_CELL_PADDING), 1)
|
||||
|
||||
gpu.font_texture = SDL.CreateGPUTexture(gpu.device, {
|
||||
type = .D2,
|
||||
format = .R8_UNORM,
|
||||
usage = {.SAMPLER},
|
||||
width = GPU_FONT_ATLAS_SIZE,
|
||||
height = GPU_FONT_ATLAS_SIZE,
|
||||
layer_count_or_depth = 1,
|
||||
num_levels = 1,
|
||||
sample_count = ._1,
|
||||
})
|
||||
gpu.font_sampler = SDL.CreateGPUSampler(gpu.device, {
|
||||
min_filter = .NEAREST,
|
||||
mag_filter = .NEAREST,
|
||||
mipmap_mode = .NEAREST,
|
||||
address_mode_u = .CLAMP_TO_EDGE,
|
||||
address_mode_v = .CLAMP_TO_EDGE,
|
||||
address_mode_w = .CLAMP_TO_EDGE,
|
||||
})
|
||||
font_transfer := SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = u32(atlas_size)})
|
||||
if gpu.font_texture == nil || gpu.font_sampler == nil || font_transfer == nil {
|
||||
fmt.println("font GPU resources failed:", SDL.GetError())
|
||||
if font_transfer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer)
|
||||
return false
|
||||
}
|
||||
defer SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer)
|
||||
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, font_transfer, false)
|
||||
if mapped == nil {
|
||||
fmt.println("font transfer map failed:", SDL.GetError())
|
||||
return false
|
||||
}
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(atlas), atlas_size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, font_transfer)
|
||||
|
||||
command := SDL.AcquireGPUCommandBuffer(gpu.device)
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUTexture(copy_pass, {
|
||||
transfer_buffer = font_transfer,
|
||||
pixels_per_row = GPU_FONT_ATLAS_SIZE,
|
||||
rows_per_layer = GPU_FONT_ATLAS_SIZE,
|
||||
}, {
|
||||
texture = gpu.font_texture,
|
||||
w = GPU_FONT_ATLAS_SIZE,
|
||||
h = GPU_FONT_ATLAS_SIZE,
|
||||
d = 1,
|
||||
}, false)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
return SDL.SubmitGPUCommandBuffer(command)
|
||||
}
|
||||
|
||||
gpu_read_font_file :: proc() -> ([]u8, string, bool) {
|
||||
for path in GPU_FONT_PATHS {
|
||||
bytes, err := os.read_entire_file(path, context.allocator)
|
||||
if err == nil && len(bytes) > 0 {
|
||||
return bytes, path, true
|
||||
}
|
||||
if err == nil {
|
||||
delete(bytes)
|
||||
}
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
gpu_push_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1: f32, c: [4]f32) {
|
||||
if len(gpu.vertices) + 6 > gpu.max_vertices do return
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y1}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y1}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y1}, c})
|
||||
}
|
||||
|
||||
gpu_push_text_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1, s0, t0, s1, t1: f32, c: [4]f32) {
|
||||
if len(gpu.text_vertices) + 6 > gpu.max_text_vertices do return
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y0}, {s0, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y1}, {s1, t1}, c})
|
||||
}
|
||||
|
||||
gpu_global_font_advance :: proc(ch: u8) -> f32 {
|
||||
return 8
|
||||
}
|
||||
|
||||
strings_has_suffix :: proc(s, suffix: string) -> bool {
|
||||
if len(suffix) > len(s) do return false
|
||||
return s[len(s) - len(suffix):] == suffix
|
||||
}
|
||||
|
||||
color_f32 :: proc(r, g, b, a: u8) -> [4]f32 {
|
||||
return {f32(r) / 255.0, f32(g) / 255.0, f32(b) / 255.0, f32(a) / 255.0}
|
||||
}
|
||||
|
||||
abs_f32 :: proc(v: f32) -> f32 {
|
||||
if v < 0 do return -v
|
||||
return v
|
||||
}
|
||||
|
||||
min_f32 :: proc(a, b: f32) -> f32 {
|
||||
if a < b do return a
|
||||
return b
|
||||
}
|
||||
|
||||
max_f32 :: proc(a, b: f32) -> f32 {
|
||||
if a > b do return a
|
||||
return b
|
||||
}
|
||||
|
||||
round_f32 :: proc(v: f32) -> f32 {
|
||||
if v >= 0 do return f32(int(v + 0.5))
|
||||
return f32(int(v - 0.5))
|
||||
}
|
||||
|
||||
ceil_f32 :: proc(v: f32) -> f32 {
|
||||
i := int(v)
|
||||
if f32(i) < v do return f32(i + 1)
|
||||
return f32(i)
|
||||
}
|
||||
184
client/odin/main.odin
Normal file
184
client/odin/main.odin
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:net"
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
|
||||
main :: proc() {
|
||||
args := os.args
|
||||
if len(args) < 2 {
|
||||
fmt.println("usage: odin run client/odin -- <port> [workspace]")
|
||||
fmt.println(" or: odin run client/odin -- --sdl [workspace] [port] [file]")
|
||||
return
|
||||
}
|
||||
|
||||
if args[1] == "--sdl" {
|
||||
workspace := "."
|
||||
if len(args) >= 3 {
|
||||
workspace = args[2]
|
||||
}
|
||||
port := 0
|
||||
file := ""
|
||||
if len(args) >= 4 {
|
||||
parsed_port, parsed_ok := strconv.parse_int(args[3])
|
||||
if parsed_ok {
|
||||
port = int(parsed_port)
|
||||
} else {
|
||||
file = args[3]
|
||||
}
|
||||
}
|
||||
if len(args) >= 5 {
|
||||
file = args[4]
|
||||
}
|
||||
run_sdl_editor(workspace, port, file)
|
||||
return
|
||||
}
|
||||
|
||||
port, ok := strconv.parse_int(args[1])
|
||||
if !ok {
|
||||
fmt.println("invalid port")
|
||||
return
|
||||
}
|
||||
|
||||
workspace := "."
|
||||
if len(args) >= 3 {
|
||||
workspace = args[2]
|
||||
}
|
||||
|
||||
run_buffer_smoke()
|
||||
editor := run_editor_smoke(workspace)
|
||||
defer editor_destroy(&editor)
|
||||
|
||||
socket, err := net.dial_tcp("127.0.0.1", int(port))
|
||||
if err != nil {
|
||||
fmt.println("connect failed:", err)
|
||||
return
|
||||
}
|
||||
defer net.close(socket)
|
||||
|
||||
send_line(socket, "{\"id\":1,\"method\":\"ping\",\"params\":{}}\n")
|
||||
read_messages(socket, 1)
|
||||
|
||||
open_workspace := fmt.tprintf("{{\"id\":2,\"method\":\"workspace/open\",\"params\":{{\"root\":%q}}}}\n", workspace)
|
||||
send_line(socket, open_workspace)
|
||||
read_messages(socket, 3)
|
||||
|
||||
send_line(socket, "{\"id\":3,\"method\":\"gradle/tasks\",\"params\":{}}\n")
|
||||
read_messages(socket, 1)
|
||||
|
||||
active := editor_active_buffer(&editor)
|
||||
diagnostics_path := active.path
|
||||
active_text := editor_active_text(&editor)
|
||||
defer delete(active_text)
|
||||
|
||||
text_change_request := fmt.tprintf("{{\"id\":4,\"method\":\"text/change\",\"params\":{{\"path\":%q,\"version\":1,\"text\":%q}}}}\n", diagnostics_path, string(active_text[:]))
|
||||
send_line(socket, text_change_request)
|
||||
read_messages(socket, 2)
|
||||
|
||||
diagnostics_request := fmt.tprintf("{{\"id\":5,\"method\":\"kotlin/diagnostics\",\"params\":{{\"path\":%q}}}}\n", diagnostics_path)
|
||||
send_line(socket, diagnostics_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
completion_request := fmt.tprintf("{{\"id\":6,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
send_line(socket, completion_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
hover_request := fmt.tprintf("{{\"id\":7,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
send_line(socket, hover_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
definition_request := fmt.tprintf("{{\"id\":8,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
send_line(socket, definition_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
references_request := fmt.tprintf("{{\"id\":9,\"method\":\"kotlin/references\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
send_line(socket, references_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
rename_request := fmt.tprintf("{{\"id\":10,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", diagnostics_path)
|
||||
send_line(socket, rename_request)
|
||||
read_messages(socket, 1)
|
||||
}
|
||||
|
||||
run_editor_smoke :: proc(workspace: string) -> Editor {
|
||||
editor := Editor{}
|
||||
path := fmt.tprintf("%s/src/main/kotlin/dev/nativeeditor/daemon/Main.kt", workspace)
|
||||
if !editor_open_file(&editor, path) {
|
||||
return editor
|
||||
}
|
||||
|
||||
fmt.println("editor-visible-before:")
|
||||
editor_print_visible(&editor, 0, 2)
|
||||
editor_replace_active_text(&editor, "fun broken( {")
|
||||
fmt.println("editor-visible-after:")
|
||||
editor_print_visible(&editor, 0, 2)
|
||||
return editor
|
||||
}
|
||||
|
||||
run_buffer_smoke :: proc() {
|
||||
buffer := buffer_make("hello world\nsecond line\n")
|
||||
defer buffer_destroy(&buffer)
|
||||
|
||||
buffer_insert(&buffer, 6, "native ")
|
||||
buffer_delete_range(&buffer, 0, 6)
|
||||
|
||||
cursor := Cursor{}
|
||||
cursor_move_to_line_col(&buffer, &cursor, 1, 6)
|
||||
cursor_insert(&buffer, &cursor, " edited")
|
||||
cursor_backspace(&buffer, &cursor)
|
||||
_ = buffer_undo(&buffer, &cursor)
|
||||
_ = buffer_redo(&buffer, &cursor)
|
||||
cursor_move_vertical(&buffer, &cursor, -1)
|
||||
|
||||
line, col := buffer_offset_to_line_col(&buffer, cursor.offset)
|
||||
second_line := buffer_line_bytes(&buffer, 1)
|
||||
defer delete(second_line)
|
||||
|
||||
text := buffer_bytes(&buffer)
|
||||
defer delete(text)
|
||||
|
||||
fmt.printf("buffer-smoke: %s", string(text[:]))
|
||||
fmt.printf("buffer-lines: %d\n", buffer_line_count(&buffer))
|
||||
fmt.printf("buffer-line-1: %s\n", string(second_line[:]))
|
||||
fmt.printf("buffer-cursor: %d:%d\n", line, col)
|
||||
}
|
||||
|
||||
send_line :: proc(socket: net.TCP_Socket, line: string) {
|
||||
bytes := transmute([]byte)line
|
||||
_, err := net.send_tcp(socket, bytes)
|
||||
if err != nil {
|
||||
fmt.println("send failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
read_messages :: proc(socket: net.TCP_Socket, expected: int) {
|
||||
seen := 0
|
||||
|
||||
for seen < expected {
|
||||
seen += read_some(socket)
|
||||
}
|
||||
}
|
||||
|
||||
read_some :: proc(socket: net.TCP_Socket) -> int {
|
||||
buf: [4096]byte
|
||||
n, err := net.recv_tcp(socket, buf[:])
|
||||
if err != nil {
|
||||
fmt.println("recv failed:", err)
|
||||
return 0
|
||||
}
|
||||
if n == 0 {
|
||||
fmt.println("connection closed")
|
||||
return 0
|
||||
}
|
||||
|
||||
lines := 0
|
||||
for b in buf[:n] {
|
||||
if b == '\n' {
|
||||
lines += 1
|
||||
}
|
||||
}
|
||||
|
||||
fmt.print(string(buf[:n]))
|
||||
return lines
|
||||
}
|
||||
3847
client/odin/sdl_app.odin
Normal file
3847
client/odin/sdl_app.odin
Normal file
File diff suppressed because it is too large
Load diff
8
client/odin/shaders/rect.frag.glsl
Normal file
8
client/odin/shaders/rect.frag.glsl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec4 v_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = v_color;
|
||||
}
|
||||
17
client/odin/shaders/rect.vert.glsl
Normal file
17
client/odin/shaders/rect.vert.glsl
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec4 a_color;
|
||||
|
||||
layout(set = 1, binding = 0) uniform VertexUniforms {
|
||||
vec2 u_viewport;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 ndc = vec2((a_pos.x / u_viewport.x) * 2.0 - 1.0,
|
||||
1.0 - (a_pos.y / u_viewport.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}
|
||||
12
client/odin/shaders/text.frag.glsl
Normal file
12
client/odin/shaders/text.frag.glsl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 1) in vec4 v_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D u_font;
|
||||
|
||||
void main() {
|
||||
float alpha = texture(u_font, v_uv).r;
|
||||
out_color = vec4(v_color.rgb, v_color.a * alpha);
|
||||
}
|
||||
20
client/odin/shaders/text.vert.glsl
Normal file
20
client/odin/shaders/text.vert.glsl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec2 a_uv;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
|
||||
layout(set = 1, binding = 0) uniform VertexUniforms {
|
||||
vec2 u_viewport;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
layout(location = 1) out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 ndc = vec2((a_pos.x / u_viewport.x) * 2.0 - 1.0,
|
||||
1.0 - (a_pos.y / u_viewport.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
v_uv = a_uv;
|
||||
v_color = a_color;
|
||||
}
|
||||
26
daemon/build.gradle.kts
Normal file
26
daemon/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
kotlin("jvm") version "2.2.21"
|
||||
application
|
||||
}
|
||||
|
||||
import org.gradle.api.file.DuplicatesStrategy
|
||||
|
||||
group = "dev.nativeeditor"
|
||||
version = "0.1.0"
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(gradleApi())
|
||||
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.21")
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("dev.nativeeditor.daemon.MainKt")
|
||||
}
|
||||
|
||||
tasks.withType<AbstractArchiveTask>().configureEach {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
15
daemon/settings.gradle.kts
Normal file
15
daemon/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "native-editor-daemon"
|
||||
1188
daemon/src/main/kotlin/dev/nativeeditor/daemon/Main.kt
Normal file
1188
daemon/src/main/kotlin/dev/nativeeditor/daemon/Main.kt
Normal file
File diff suppressed because it is too large
Load diff
40
docs/shader-toolchain.md
Normal file
40
docs/shader-toolchain.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# SDL3 GPU Shader Toolchain
|
||||
|
||||
SDL3 GPU accepts different shader formats depending on the selected backend:
|
||||
|
||||
- Vulkan: SPIR-V
|
||||
- D3D12: DXBC or DXIL
|
||||
- Metal: MSL or metallib
|
||||
|
||||
For this Linux-first prototype, use GLSL compiled to SPIR-V with `glslc` from shaderc.
|
||||
|
||||
```sh
|
||||
scripts/compile-shaders.sh
|
||||
```
|
||||
|
||||
Outputs are written to `client/odin/shaders/compiled/` and intentionally ignored by git.
|
||||
|
||||
Fedora install:
|
||||
|
||||
```sh
|
||||
sudo dnf install glslc
|
||||
```
|
||||
|
||||
Cross-platform path later:
|
||||
|
||||
- Use SDL_shadercross for offline conversion from SPIR-V or HLSL to backend-specific shader formats.
|
||||
- Keep source shaders in git.
|
||||
- Generate SPIR-V/DXIL/MSL in CI or release packaging.
|
||||
- At runtime, choose the shader blob matching `SDL.GetGPUDeviceDriver(device)` or `SDL.GetGPUShaderFormats(device)`.
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- Build Linux/Vulkan first with checked-in GLSL sources and generated SPIR-V as build artifacts.
|
||||
- Add SDL_shadercross only when Windows/macOS backends matter.
|
||||
|
||||
Current renderer state:
|
||||
|
||||
- The editor loads `rect.vert.spv` and `rect.frag.spv` for the direct SDL3 GPU path.
|
||||
- The editor loads `text.vert.spv` and `text.frag.spv` for font-atlas text.
|
||||
- Text is rendered from a baked `stb_truetype` atlas using `/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf`.
|
||||
- A later portability step should bundle a project font or add configurable font discovery.
|
||||
208
protocol.md
Normal file
208
protocol.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# TCP Protocol
|
||||
|
||||
Transport: UTF-8 JSON objects separated by `\n`.
|
||||
|
||||
File paths in text and intelligence requests are normalized to absolute daemon-side paths before being used as document keys.
|
||||
|
||||
## Request
|
||||
|
||||
```json
|
||||
{"id":1,"method":"workspace/open","params":{"root":"/home/me/project"}}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
```json
|
||||
{"id":1,"ok":true,"result":{"root":"/home/me/project","gradle":true,"modules":[],"sourceRoots":[],"tasks":[]}}
|
||||
```
|
||||
|
||||
## Error
|
||||
|
||||
```json
|
||||
{"id":1,"ok":false,"error":{"code":"UNKNOWN_METHOD","message":"No handler for method"}}
|
||||
```
|
||||
|
||||
## Event
|
||||
|
||||
```json
|
||||
{"event":"workspace/indexing","params":{"state":"started"}}
|
||||
```
|
||||
|
||||
Diagnostics are also published asynchronously after `text/open` and `text/change`:
|
||||
|
||||
```json
|
||||
{"event":"diagnostics/publish","params":{"path":"/home/me/project/src/main/kotlin/App.kt","version":7,"diagnostics":[]}}
|
||||
```
|
||||
|
||||
## Current Methods
|
||||
|
||||
The `kotlin/*` names are historical protocol names. Diagnostics and heuristic intelligence currently also handle `.java` files where noted.
|
||||
|
||||
- `ping`
|
||||
- `workspace/open`
|
||||
- `workspace/close`
|
||||
- `text/open`
|
||||
- `text/change`
|
||||
- `text/close`
|
||||
- `kotlin/diagnostics`
|
||||
- `kotlin/completion`
|
||||
- `kotlin/definition`
|
||||
- `kotlin/references`
|
||||
- `kotlin/rename`
|
||||
- `kotlin/hover`
|
||||
- `gradle/tasks`
|
||||
- `gradle/run`
|
||||
|
||||
## `workspace/open` Result
|
||||
|
||||
Module `sourceRoots` and `testSourceRoots` include Gradle IdeaModel roots plus common generated Kotlin/Java source directories under `build/generated/` when those directories already exist.
|
||||
|
||||
```json
|
||||
{
|
||||
"root": "/home/me/project",
|
||||
"gradle": true,
|
||||
"modules": [
|
||||
{
|
||||
"name": "app",
|
||||
"gradlePath": ":app",
|
||||
"directory": "/home/me/project/app",
|
||||
"sourceRoots": [],
|
||||
"testSourceRoots": [],
|
||||
"resourceRoots": [],
|
||||
"testResourceRoots": []
|
||||
}
|
||||
],
|
||||
"sourceRoots": [],
|
||||
"tasks": []
|
||||
}
|
||||
```
|
||||
|
||||
## `gradle/run` Request
|
||||
|
||||
```json
|
||||
{"id":2,"method":"gradle/run","params":{"task":":app:test"}}
|
||||
```
|
||||
|
||||
## `kotlin/diagnostics` Request
|
||||
|
||||
Supports Kotlin and Java files. Kotlin diagnostics use the embedded Kotlin compiler; Java diagnostics use the JDK compiler. Main files use main source roots, while test files use main plus test source roots. Existing module output directories under `build/classes/` and `build/resources/` are added to the compiler classpath.
|
||||
|
||||
```json
|
||||
{"id":3,"method":"kotlin/diagnostics","params":{"path":"/home/me/project/src/main/kotlin/App.kt"}}
|
||||
```
|
||||
|
||||
## `kotlin/diagnostics` Result
|
||||
|
||||
```json
|
||||
{
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "error",
|
||||
"message": "Syntax error",
|
||||
"path": "/home/me/project/src/main/kotlin/App.kt",
|
||||
"line": 1,
|
||||
"column": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `kotlin/completion` Request
|
||||
|
||||
Returns prefix-filtered Kotlin/Java keywords plus heuristic declarations and identifiers from the current buffer and workspace.
|
||||
|
||||
```json
|
||||
{"id":4,"method":"kotlin/completion","params":{"path":"/home/me/project/src/main/kotlin/App.kt","line":10,"column":8}}
|
||||
```
|
||||
|
||||
`line` and `column` are 1-based.
|
||||
|
||||
## `kotlin/completion` Result
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"label": "fun", "kind": "keyword"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `kotlin/hover` Request
|
||||
|
||||
Returns keyword or heuristic Kotlin/Java declaration information for the identifier at the requested position.
|
||||
|
||||
```json
|
||||
{"id":5,"method":"kotlin/hover","params":{"path":"/home/me/project/src/main/kotlin/App.kt","line":10,"column":8}}
|
||||
```
|
||||
|
||||
`line` and `column` are 1-based.
|
||||
|
||||
## `kotlin/hover` Result
|
||||
|
||||
```json
|
||||
{"contents":"Kotlin keyword `fun`"}
|
||||
```
|
||||
|
||||
`contents` is `null` when no hover information is available.
|
||||
|
||||
## `kotlin/definition` Request
|
||||
|
||||
Returns heuristic Kotlin/Java declaration locations for the identifier at the requested position.
|
||||
|
||||
```json
|
||||
{"id":6,"method":"kotlin/definition","params":{"path":"/home/me/project/src/main/kotlin/App.kt","line":10,"column":8}}
|
||||
```
|
||||
|
||||
`line` and `column` are 1-based.
|
||||
|
||||
## `kotlin/definition` Result
|
||||
|
||||
```json
|
||||
{
|
||||
"locations": [
|
||||
{"path": "/home/me/project/src/main/kotlin/App.kt", "line": 4, "column": 7}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `kotlin/references` Request
|
||||
|
||||
Returns heuristic Kotlin/Java text references for the identifier at the requested position.
|
||||
|
||||
```json
|
||||
{"id":7,"method":"kotlin/references","params":{"path":"/home/me/project/src/main/kotlin/App.kt","line":10,"column":8}}
|
||||
```
|
||||
|
||||
`line` and `column` are 1-based.
|
||||
|
||||
## `kotlin/references` Result
|
||||
|
||||
```json
|
||||
{
|
||||
"locations": [
|
||||
{"path": "/home/me/project/src/main/kotlin/App.kt", "line": 4, "column": 7}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `kotlin/rename` Request
|
||||
|
||||
```json
|
||||
{"id":8,"method":"kotlin/rename","params":{"path":"/home/me/project/src/main/kotlin/App.kt","line":10,"column":8,"newName":"newSymbol"}}
|
||||
```
|
||||
|
||||
This is currently preview-only. It does not modify files.
|
||||
`newName` must be a valid identifier and must not be a Kotlin or Java keyword.
|
||||
|
||||
## `kotlin/rename` Result
|
||||
|
||||
```json
|
||||
{
|
||||
"applied": false,
|
||||
"oldName": "oldSymbol",
|
||||
"newName": "newSymbol",
|
||||
"edits": [
|
||||
{"path": "/home/me/project/src/main/kotlin/App.kt", "line": 4, "column": 7, "oldText": "oldSymbol", "newText": "newSymbol"}
|
||||
]
|
||||
}
|
||||
```
|
||||
73
scripts/autopilot-opencode.sh
Executable file
73
scripts/autopilot-opencode.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Repeatedly asks opencode to continue autonomous project work.
|
||||
#
|
||||
# Configuration:
|
||||
# OPENCODE_BIN Command to run. Default: opencode
|
||||
# OPENCODE_PROJECT Project/workspace path. Default: current directory
|
||||
# OPENCODE_SESSION Session id to continue. If unset, uses --continue
|
||||
# OPENCODE_MODEL Optional model, e.g. openai/gpt-5.5
|
||||
# OPENCODE_AGENT Optional agent name
|
||||
# OPENCODE_EXTRA_ARGS Optional extra args inserted before the message, shell-split
|
||||
# AUTOPILOT_MESSAGE Message sent each iteration. Default: keep working prompt below
|
||||
# AUTOPILOT_INTERVAL Seconds to sleep between successful iterations. Default: 10
|
||||
# AUTOPILOT_MAX_RUNS Max iterations. 0 means forever. Default: 0
|
||||
# AUTOPILOT_STOP_FILE Stop when this file exists. Default: .opencode-autopilot-stop
|
||||
# AUTOPILOT_ALLOW_PERMISSIONS Automatically approve tool permissions. Default: 1
|
||||
|
||||
OPENCODE_BIN=${OPENCODE_BIN:-opencode}
|
||||
OPENCODE_PROJECT=${OPENCODE_PROJECT:-$(pwd)}
|
||||
AUTOPILOT_INTERVAL=${AUTOPILOT_INTERVAL:-10}
|
||||
AUTOPILOT_MAX_RUNS=${AUTOPILOT_MAX_RUNS:-0}
|
||||
AUTOPILOT_STOP_FILE=${AUTOPILOT_STOP_FILE:-.opencode-autopilot-stop}
|
||||
AUTOPILOT_ALLOW_PERMISSIONS=${AUTOPILOT_ALLOW_PERMISSIONS:-1}
|
||||
AUTOPILOT_MESSAGE=${AUTOPILOT_MESSAGE:-Continue working autonomously in this configured session. Keep working on the current task list. If every task is finished, inspect the project, decide the next highest-value tasks, add or update the task list, implement them, verify them, and continue until blocked. Do not stop just because one task is complete; stop only for a real blocker that requires user input.}
|
||||
|
||||
run_count=0
|
||||
|
||||
while true; do
|
||||
if [[ -e "$AUTOPILOT_STOP_FILE" ]]; then
|
||||
printf 'stop file exists: %s\n' "$AUTOPILOT_STOP_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$AUTOPILOT_MAX_RUNS" != "0" && "$run_count" -ge "$AUTOPILOT_MAX_RUNS" ]]; then
|
||||
printf 'reached AUTOPILOT_MAX_RUNS=%s\n' "$AUTOPILOT_MAX_RUNS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
args=(run --dir "$OPENCODE_PROJECT")
|
||||
|
||||
if [[ "$AUTOPILOT_ALLOW_PERMISSIONS" != "0" ]]; then
|
||||
args+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
|
||||
if [[ -n "${OPENCODE_SESSION:-}" ]]; then
|
||||
args+=(--session "$OPENCODE_SESSION")
|
||||
else
|
||||
args+=(--continue)
|
||||
fi
|
||||
|
||||
if [[ -n "${OPENCODE_MODEL:-}" ]]; then
|
||||
args+=(--model "$OPENCODE_MODEL")
|
||||
fi
|
||||
|
||||
if [[ -n "${OPENCODE_AGENT:-}" ]]; then
|
||||
args+=(--agent "$OPENCODE_AGENT")
|
||||
fi
|
||||
|
||||
if [[ -n "${OPENCODE_EXTRA_ARGS:-}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
extra_args=($OPENCODE_EXTRA_ARGS)
|
||||
args+=("${extra_args[@]}")
|
||||
fi
|
||||
|
||||
args+=("$AUTOPILOT_MESSAGE")
|
||||
|
||||
run_count=$((run_count + 1))
|
||||
printf '[%s] autopilot iteration %d\n' "$(date -Is)" "$run_count"
|
||||
"$OPENCODE_BIN" "${args[@]}"
|
||||
|
||||
sleep "$AUTOPILOT_INTERVAL"
|
||||
done
|
||||
20
scripts/compile-shaders.sh
Executable file
20
scripts/compile-shaders.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SRC="$ROOT/client/odin/shaders"
|
||||
OUT="$SRC/compiled"
|
||||
|
||||
if ! command -v glslc >/dev/null 2>&1; then
|
||||
printf 'error: glslc not found. Install shaderc/glslc. On Fedora: sudo dnf install glslc\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
glslc -fshader-stage=vert "$SRC/rect.vert.glsl" -o "$OUT/rect.vert.spv"
|
||||
glslc -fshader-stage=frag "$SRC/rect.frag.glsl" -o "$OUT/rect.frag.spv"
|
||||
glslc -fshader-stage=vert "$SRC/text.vert.glsl" -o "$OUT/text.vert.spv"
|
||||
glslc -fshader-stage=frag "$SRC/text.frag.glsl" -o "$OUT/text.frag.spv"
|
||||
|
||||
printf 'compiled SDL3 GPU shaders to %s\n' "$OUT"
|
||||
Loading…
Add table
Add a link
Reference in a new issue