feat: add source history tools to world client
Add committed revision browsing, syntax-highlighted source editing, revision previews, and confirmed rollbacks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
fc33332e7d
commit
2f9d500ca3
|
|
@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file.
|
||||||
views.
|
views.
|
||||||
- Extend the graphical client with source editing, compilation diagnostics,
|
- Extend the graphical client with source editing, compilation diagnostics,
|
||||||
source commits, and explicit image-event collection.
|
source commits, and explicit image-event collection.
|
||||||
|
- Add committed source history browsing, revision preview, and confirmed
|
||||||
|
rollback to the graphical source editor.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@ actor browsing and inspection, message sending, lifecycle controls, explicit
|
||||||
scheduler ticks, bounded debugger stepping, source breakpoints,
|
scheduler ticks, bounded debugger stepping, source breakpoints,
|
||||||
execution-stack inspection, and retained logs. Its source view supports
|
execution-stack inspection, and retained logs. Its source view supports
|
||||||
editing image sources with optimistic conflict checks, compilation diagnostics,
|
editing image sources with optimistic conflict checks, compilation diagnostics,
|
||||||
and explicit history commits. The events view has a manual collection action:
|
explicit history commits, and revision preview/rollback. Rollback requires
|
||||||
|
confirmation and creates a new current source version without deleting history.
|
||||||
|
The events view has a manual collection action:
|
||||||
collecting events consumes the image's shared event queue, so other clients do
|
collecting events consumes the image's shared event queue, so other clients do
|
||||||
not receive events collected by the world client. Rune frame locals and
|
not receive events collected by the world client. Rune frame locals and
|
||||||
expression evaluation are not available. The client does not create or embed a
|
expression evaluation are not available. The client does not create or embed a
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use runa_client::WebSocketClient;
|
||||||
use runa_core::{ActorRef, StepMode, Value};
|
use runa_core::{ActorRef, StepMode, Value};
|
||||||
use runa_protocol::{
|
use runa_protocol::{
|
||||||
ActorSnapshotDto, ActorStatusDto, Command, EventDto, ExecutionInspectionDto, LogEntry, Reply,
|
ActorSnapshotDto, ActorStatusDto, Command, EventDto, ExecutionInspectionDto, LogEntry, Reply,
|
||||||
SourceDiagnostic, SourceFileDto, SystemMessageDto,
|
SourceCommitDto, SourceDiagnostic, SourceFileDto, SystemMessageDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -51,6 +51,15 @@ enum WorkerUpdate {
|
||||||
submitted_source: String,
|
submitted_source: String,
|
||||||
},
|
},
|
||||||
SourceDiagnostics(Vec<SourceDiagnostic>),
|
SourceDiagnostics(Vec<SourceDiagnostic>),
|
||||||
|
SourceHistory {
|
||||||
|
path: String,
|
||||||
|
history: Vec<SourceCommitDto>,
|
||||||
|
},
|
||||||
|
RevisionSource {
|
||||||
|
path: String,
|
||||||
|
revision: String,
|
||||||
|
source: String,
|
||||||
|
},
|
||||||
Events(Vec<EventDto>),
|
Events(Vec<EventDto>),
|
||||||
Snapshot(Box<WorkerSnapshot>),
|
Snapshot(Box<WorkerSnapshot>),
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +105,10 @@ struct WorldApp {
|
||||||
source_draft: String,
|
source_draft: String,
|
||||||
source_diagnostics: Vec<SourceDiagnostic>,
|
source_diagnostics: Vec<SourceDiagnostic>,
|
||||||
source_dirty: bool,
|
source_dirty: bool,
|
||||||
|
source_history: Vec<SourceCommitDto>,
|
||||||
|
selected_revision: Option<String>,
|
||||||
|
revision_preview: Option<(String, String)>,
|
||||||
|
rollback_confirmation: Option<(String, String)>,
|
||||||
events: Vec<EventDto>,
|
events: Vec<EventDto>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +147,10 @@ impl WorldApp {
|
||||||
source_draft: String::new(),
|
source_draft: String::new(),
|
||||||
source_diagnostics: Vec::new(),
|
source_diagnostics: Vec::new(),
|
||||||
source_dirty: false,
|
source_dirty: false,
|
||||||
|
source_history: Vec::new(),
|
||||||
|
selected_revision: None,
|
||||||
|
revision_preview: None,
|
||||||
|
rollback_confirmation: None,
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +181,16 @@ impl WorldApp {
|
||||||
self.source = None;
|
self.source = None;
|
||||||
self.loading_source = Some(path.to_owned());
|
self.loading_source = Some(path.to_owned());
|
||||||
self.source_diagnostics.clear();
|
self.source_diagnostics.clear();
|
||||||
|
self.source_history.clear();
|
||||||
|
self.selected_revision = None;
|
||||||
|
self.revision_preview = None;
|
||||||
self.source_dirty = false;
|
self.source_dirty = false;
|
||||||
self.submit(Command::ReadSource {
|
self.submit(Command::ReadSource {
|
||||||
path: path.to_owned(),
|
path: path.to_owned(),
|
||||||
});
|
});
|
||||||
|
self.submit(Command::SourceHistory {
|
||||||
|
path: path.to_owned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_worker_updates(&mut self) {
|
fn read_worker_updates(&mut self) {
|
||||||
|
|
@ -186,6 +209,9 @@ impl WorldApp {
|
||||||
self.loading_source = None;
|
self.loading_source = None;
|
||||||
self.source_dirty = false;
|
self.source_dirty = false;
|
||||||
self.source_diagnostics.clear();
|
self.source_diagnostics.clear();
|
||||||
|
self.source_history.clear();
|
||||||
|
self.selected_revision = None;
|
||||||
|
self.revision_preview = None;
|
||||||
self.events.clear();
|
self.events.clear();
|
||||||
}
|
}
|
||||||
Ok(WorkerUpdate::SourceLoaded(source)) => {
|
Ok(WorkerUpdate::SourceLoaded(source)) => {
|
||||||
|
|
@ -213,6 +239,27 @@ impl WorldApp {
|
||||||
Ok(WorkerUpdate::SourceDiagnostics(diagnostics)) => {
|
Ok(WorkerUpdate::SourceDiagnostics(diagnostics)) => {
|
||||||
self.source_diagnostics = diagnostics;
|
self.source_diagnostics = diagnostics;
|
||||||
}
|
}
|
||||||
|
Ok(WorkerUpdate::SourceHistory { path, history }) => {
|
||||||
|
if self.source_path() != Some(path.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(selected) = &self.selected_revision
|
||||||
|
&& !history.iter().any(|entry| &entry.revision == selected)
|
||||||
|
{
|
||||||
|
self.selected_revision = None;
|
||||||
|
self.revision_preview = None;
|
||||||
|
}
|
||||||
|
self.source_history = history;
|
||||||
|
}
|
||||||
|
Ok(WorkerUpdate::RevisionSource {
|
||||||
|
path,
|
||||||
|
revision,
|
||||||
|
source,
|
||||||
|
}) => {
|
||||||
|
if self.source_path() == Some(path.as_str()) {
|
||||||
|
self.revision_preview = Some((revision, source));
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(WorkerUpdate::Events(events)) => {
|
Ok(WorkerUpdate::Events(events)) => {
|
||||||
self.events.extend(events);
|
self.events.extend(events);
|
||||||
if self.events.len() > 200 {
|
if self.events.len() > 200 {
|
||||||
|
|
@ -566,7 +613,7 @@ impl WorldApp {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_source_editor(&mut self, ui: &mut egui::Ui) {
|
fn draw_source_editor(&mut self, context: &egui::Context, ui: &mut egui::Ui) {
|
||||||
ui.heading("Source editor");
|
ui.heading("Source editor");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Source file");
|
ui.label("Source file");
|
||||||
|
|
@ -624,20 +671,114 @@ impl WorldApp {
|
||||||
egui::Button::new("Commit"),
|
egui::Button::new("Commit"),
|
||||||
)
|
)
|
||||||
.clicked()
|
.clicked()
|
||||||
&& let Some(source) = &self.source
|
&& let Some(path) = self.source.as_ref().map(|source| source.path.clone())
|
||||||
{
|
{
|
||||||
self.submit(Command::CommitSource {
|
self.submit(Command::CommitSource { path: path.clone() });
|
||||||
path: source.path.clone(),
|
self.submit(Command::SourceHistory { path });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ui.collapsing(
|
||||||
|
format!("Committed history ({})", self.source_history.len()),
|
||||||
|
|ui| {
|
||||||
|
if ui.button("Refresh history").clicked()
|
||||||
|
&& let Some(source) = &self.source
|
||||||
|
{
|
||||||
|
self.submit(Command::SourceHistory {
|
||||||
|
path: source.path.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.source_history.is_empty() {
|
||||||
|
ui.label("No committed revisions yet.");
|
||||||
|
}
|
||||||
|
egui::ComboBox::from_id_salt("source_revision")
|
||||||
|
.selected_text(
|
||||||
|
self.selected_revision
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("Choose a revision"),
|
||||||
|
)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for item in &self.source_history {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.selected_revision,
|
||||||
|
Some(item.revision.clone()),
|
||||||
|
format!("{} · {}", item.revision, item.source_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let has_revision = self.selected_revision.is_some();
|
||||||
|
if ui
|
||||||
|
.add_enabled(has_revision, egui::Button::new("Preview"))
|
||||||
|
.clicked()
|
||||||
|
&& let (Some(source), Some(revision)) =
|
||||||
|
(&self.source, &self.selected_revision)
|
||||||
|
{
|
||||||
|
self.submit(Command::RevisionSource {
|
||||||
|
path: source.path.clone(),
|
||||||
|
revision: revision.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
has_revision && !self.source_dirty,
|
||||||
|
egui::Button::new("Roll back to revision"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
&& let (Some(source), Some(revision)) =
|
||||||
|
(&self.source, &self.selected_revision)
|
||||||
|
{
|
||||||
|
self.rollback_confirmation = Some((source.path.clone(), revision.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some((revision, preview)) = &self.revision_preview {
|
||||||
|
ui.label(format!("Previewing {revision}"));
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(180.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.monospace(preview);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if let Some((path, revision)) = self.rollback_confirmation.clone() {
|
||||||
|
egui::Window::new("Confirm source rollback")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.show(context, |ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"Replace `{path}` with committed revision {revision}? This writes a new current source version; it does not delete history."
|
||||||
|
));
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.add_enabled(!self.source_dirty, egui::Button::new("Roll back"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.submit(Command::RollbackSource {
|
||||||
|
path: path.clone(),
|
||||||
|
revision: revision.clone(),
|
||||||
|
});
|
||||||
|
self.submit(Command::SourceHistory { path });
|
||||||
|
self.rollback_confirmation = None;
|
||||||
|
}
|
||||||
|
if ui.button("Cancel").clicked() {
|
||||||
|
self.rollback_confirmation = None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
if self.source.is_none() {
|
if self.source.is_none() {
|
||||||
ui.label("Choose a source file to inspect or edit.");
|
ui.label("Choose a source file to inspect or edit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut layouter = |ui: &egui::Ui, source: &str, wrap_width: f32| {
|
||||||
|
let mut job = rune_layout_job(source);
|
||||||
|
job.wrap.max_width = wrap_width;
|
||||||
|
ui.fonts(|fonts| fonts.layout_job(job))
|
||||||
|
};
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
egui::TextEdit::multiline(&mut self.source_draft)
|
egui::TextEdit::multiline(&mut self.source_draft)
|
||||||
.font(egui::TextStyle::Monospace)
|
.code_editor()
|
||||||
|
.layouter(&mut layouter)
|
||||||
.desired_width(f32::INFINITY)
|
.desired_width(f32::INFINITY)
|
||||||
.desired_rows(24),
|
.desired_rows(24),
|
||||||
);
|
);
|
||||||
|
|
@ -692,7 +833,7 @@ impl eframe::App for WorldApp {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
match self.view {
|
match self.view {
|
||||||
WorldView::Actors => self.draw_inspector(ui),
|
WorldView::Actors => self.draw_inspector(ui),
|
||||||
WorldView::Sources => self.draw_source_editor(ui),
|
WorldView::Sources => self.draw_source_editor(context, ui),
|
||||||
WorldView::Events => self.draw_events(ui),
|
WorldView::Events => self.draw_events(ui),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -769,12 +910,29 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
Command::CheckSource { .. } => "check_source",
|
Command::CheckSource { .. } => "check_source",
|
||||||
Command::GetEvents => "get_events",
|
Command::GetEvents => "get_events",
|
||||||
Command::CommitSource { .. } => "commit_source",
|
Command::CommitSource { .. } => "commit_source",
|
||||||
|
Command::SourceHistory { .. } => "source_history",
|
||||||
|
Command::RevisionSource { .. } => "revision_source",
|
||||||
|
Command::RollbackSource { .. } => "rollback_source",
|
||||||
_ => "command",
|
_ => "command",
|
||||||
};
|
};
|
||||||
let submitted_source = match &command {
|
let submitted_source = match &command {
|
||||||
Command::WriteSource { source, .. } => Some(source.clone()),
|
Command::WriteSource { source, .. } => Some(source.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let revision = match &command {
|
||||||
|
Command::RevisionSource { revision, .. }
|
||||||
|
| Command::RollbackSource { revision, .. } => Some(revision.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let source_path = match &command {
|
||||||
|
Command::ReadSource { path }
|
||||||
|
| Command::WriteSource { path, .. }
|
||||||
|
| Command::CommitSource { path }
|
||||||
|
| Command::SourceHistory { path }
|
||||||
|
| Command::RevisionSource { path, .. }
|
||||||
|
| Command::RollbackSource { path, .. } => Some(path.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
match request_reply(connected, command) {
|
match request_reply(connected, command) {
|
||||||
Ok(reply) => {
|
Ok(reply) => {
|
||||||
let update = match (operation, reply) {
|
let update = match (operation, reply) {
|
||||||
|
|
@ -791,6 +949,20 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
("check_source", Reply::SourceChecked { diagnostics }) => {
|
("check_source", Reply::SourceChecked { diagnostics }) => {
|
||||||
Some(WorkerUpdate::SourceDiagnostics(diagnostics))
|
Some(WorkerUpdate::SourceDiagnostics(diagnostics))
|
||||||
}
|
}
|
||||||
|
("source_history", Reply::SourceHistory(history)) => source_path
|
||||||
|
.map(|path| WorkerUpdate::SourceHistory { path, history }),
|
||||||
|
("revision_source", Reply::BehaviorSource { source }) => {
|
||||||
|
match (source_path, revision) {
|
||||||
|
(Some(path), Some(revision)) => {
|
||||||
|
Some(WorkerUpdate::RevisionSource {
|
||||||
|
path,
|
||||||
|
revision,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
("get_events", Reply::Events(events)) => {
|
("get_events", Reply::Events(events)) => {
|
||||||
Some(WorkerUpdate::Events(events))
|
Some(WorkerUpdate::Events(events))
|
||||||
}
|
}
|
||||||
|
|
@ -800,6 +972,13 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
commit.revision
|
commit.revision
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
("rollback_source", Reply::Source(source)) => {
|
||||||
|
let submitted_source = source.source.clone();
|
||||||
|
Some(WorkerUpdate::SourceSaved {
|
||||||
|
source,
|
||||||
|
submitted_source,
|
||||||
|
})
|
||||||
|
}
|
||||||
(_, reply) => Some(WorkerUpdate::Notice(format!(
|
(_, reply) => Some(WorkerUpdate::Notice(format!(
|
||||||
"Completed with unexpected response: {reply:?}"
|
"Completed with unexpected response: {reply:?}"
|
||||||
))),
|
))),
|
||||||
|
|
@ -1031,6 +1210,96 @@ fn show_json(ui: &mut egui::Ui, value: &Value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rune_layout_job(source: &str) -> egui::text::LayoutJob {
|
||||||
|
let mut job = egui::text::LayoutJob::default();
|
||||||
|
let mut cursor = 0;
|
||||||
|
while cursor < source.len() {
|
||||||
|
let remaining = &source[cursor..];
|
||||||
|
let (end, color) = if remaining.starts_with("//") {
|
||||||
|
(
|
||||||
|
source[cursor..]
|
||||||
|
.find('\n')
|
||||||
|
.map_or(source.len(), |offset| cursor + offset),
|
||||||
|
egui::Color32::from_rgb(125, 155, 125),
|
||||||
|
)
|
||||||
|
} else if remaining.starts_with('"') {
|
||||||
|
let mut end = cursor + 1;
|
||||||
|
let mut escaped = false;
|
||||||
|
for character in source[end..].chars() {
|
||||||
|
end += character.len_utf8();
|
||||||
|
if character == '"' && !escaped {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
escaped = character == '\\' && !escaped;
|
||||||
|
if character != '\\' {
|
||||||
|
escaped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(end, egui::Color32::from_rgb(220, 170, 110))
|
||||||
|
} else {
|
||||||
|
let character = remaining.chars().next().expect("cursor is within source");
|
||||||
|
if character.is_ascii_alphabetic() || character == '_' {
|
||||||
|
let mut end = cursor + character.len_utf8();
|
||||||
|
for next in source[end..].chars() {
|
||||||
|
if next.is_ascii_alphanumeric() || next == '_' {
|
||||||
|
end += next.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let token = &source[cursor..end];
|
||||||
|
let color = if matches!(
|
||||||
|
token,
|
||||||
|
"pub"
|
||||||
|
| "fn"
|
||||||
|
| "let"
|
||||||
|
| "mut"
|
||||||
|
| "if"
|
||||||
|
| "else"
|
||||||
|
| "loop"
|
||||||
|
| "while"
|
||||||
|
| "match"
|
||||||
|
| "return"
|
||||||
|
| "async"
|
||||||
|
| "await"
|
||||||
|
| "select"
|
||||||
|
) {
|
||||||
|
egui::Color32::from_rgb(190, 145, 245)
|
||||||
|
} else {
|
||||||
|
egui::Color32::from_rgb(215, 215, 220)
|
||||||
|
};
|
||||||
|
(end, color)
|
||||||
|
} else if character.is_ascii_digit() {
|
||||||
|
let mut end = cursor + character.len_utf8();
|
||||||
|
for next in source[end..].chars() {
|
||||||
|
if next.is_ascii_digit() {
|
||||||
|
end += next.len_utf8();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(end, egui::Color32::from_rgb(130, 185, 225))
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
cursor + character.len_utf8(),
|
||||||
|
egui::Color32::from_rgb(215, 215, 220),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
job.append(
|
||||||
|
&source[cursor..end],
|
||||||
|
0.0,
|
||||||
|
egui::TextFormat {
|
||||||
|
font_id: egui::FontId::monospace(14.0),
|
||||||
|
color,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cursor = end;
|
||||||
|
}
|
||||||
|
job
|
||||||
|
}
|
||||||
|
|
||||||
fn to_json_value(value: &Value) -> serde_json::Value {
|
fn to_json_value(value: &Value) -> serde_json::Value {
|
||||||
match value {
|
match value {
|
||||||
Value::Null => serde_json::Value::Null,
|
Value::Null => serde_json::Value::Null,
|
||||||
|
|
@ -1096,4 +1365,23 @@ mod tests {
|
||||||
let json = serde_json::to_string(&to_json_value(&value)).unwrap();
|
let json = serde_json::to_string(&to_json_value(&value)).unwrap();
|
||||||
assert_eq!(parse_value(&json).unwrap(), value);
|
assert_eq!(parse_value(&json).unwrap(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rune_highlighting_preserves_source_and_marks_keywords_and_comments() {
|
||||||
|
let source = "pub fn handle() { // note\n let n = 12;\n}";
|
||||||
|
let layout = rune_layout_job(source);
|
||||||
|
assert_eq!(layout.text, source);
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.any(|section| &source[section.byte_range.clone()] == "pub")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.any(|section| &source[section.byte_range.clone()] == "// note")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,11 +223,12 @@ The first native client milestone uses egui/eframe and connects to the existing
|
||||||
loopback image over the shared WebSocket protocol. It currently provides an
|
loopback image over the shared WebSocket protocol. It currently provides an
|
||||||
actor browser and inspector with message, lifecycle, log, breakpoint, bounded
|
actor browser and inspector with message, lifecycle, log, breakpoint, bounded
|
||||||
step, and execution-stack controls, plus a source editor with compilation
|
step, and execution-stack controls, plus a source editor with compilation
|
||||||
diagnostics and explicit source commits. Image events can be collected
|
diagnostics, explicit source commits, committed-revision previews, and
|
||||||
manually; this operation drains the shared queue and therefore consumes events
|
confirmed rollback. Image events can be collected manually; this operation
|
||||||
for other clients too. Network requests run on a worker thread, not the UI
|
drains the shared queue and therefore consumes events for other clients too.
|
||||||
frame loop. This is a client foundation, not yet a Rune-composed graphical
|
Network requests run on a worker thread, not the UI frame loop. This is a
|
||||||
world; frame locals and expression evaluation remain unavailable.
|
client foundation, not yet a Rune-composed graphical world; frame locals and
|
||||||
|
expression evaluation remain unavailable.
|
||||||
|
|
||||||
## 8. Unified Programmatic Image API
|
## 8. Unified Programmatic Image API
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue