Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/editor/src/display_map/inlay_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,33 @@ impl<'a> Iterator for InlayChunks<'a> {
}),
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
InlayId::ReplResult(_) => {
let text = inlay.text().to_string();
renderer = Some(ChunkRenderer {
id: ChunkRendererId::Inlay(inlay.id),
render: Arc::new(move |cx| {
let colors = cx.theme().colors();
div()
.flex()
.flex_row()
.items_center()
.child(div().w_4())
.child(
div()
.px_1()
.rounded_sm()
.bg(colors.surface_background)
.text_color(colors.text_muted)
.text_xs()
.child(text.trim().to_string()),
)
.into_any_element()
}),
constrain_width: false,
measured_width: None,
});
self.highlight_styles.inlay_hint
}
InlayId::Color(_) => {
if let InlayContent::Color(color) = inlay.content {
renderer = Some(ChunkRenderer {
Expand Down
8 changes: 8 additions & 0 deletions crates/editor/src/inlays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ impl Inlay {
}
}

pub fn repl_result<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::ReplResult(id),
position,
content: InlayContent::Text(text.into()),
}
}

pub fn text(&self) -> &Rope {
static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
match &self.content {
Expand Down
2 changes: 2 additions & 0 deletions crates/project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ pub enum InlayId {
// LSP
Hint(usize),
Color(usize),
ReplResult(usize),
}

impl InlayId {
Expand All @@ -422,6 +423,7 @@ impl InlayId {
Self::DebuggerValue(id) => *id,
Self::Hint(id) => *id,
Self::Color(id) => *id,
Self::ReplResult(id) => *id,
}
}
}
Expand Down
51 changes: 49 additions & 2 deletions crates/repl/src/outputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
//! interpreting and displaying various types of Jupyter output.

use editor::{Editor, MultiBuffer};
use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity};
use gpui::{AnyElement, ClipboardItem, Entity, EventEmitter, Render, WeakEntity};
use language::Buffer;
use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
use ui::{
Expand All @@ -58,6 +58,9 @@ pub(crate) mod user_error;
use user_error::ErrorView;
use workspace::Workspace;

use crate::repl_settings::ReplSettings;
use settings::Settings;

/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
fn rank_mime_type(mimetype: &MimeType) -> usize {
match mimetype {
Expand Down Expand Up @@ -389,6 +392,9 @@ pub enum ExecutionStatus {
Restarting,
}

pub struct ExecutionViewFinishedEmpty;
pub struct ExecutionViewFinishedSmall(pub String);

/// An ExecutionView shows the outputs of an execution.
/// It can hold zero or more outputs, which the user
/// sees as "the output" for a single execution.
Expand All @@ -399,6 +405,9 @@ pub struct ExecutionView {
pub status: ExecutionStatus,
}

impl EventEmitter<ExecutionViewFinishedEmpty> for ExecutionView {}
impl EventEmitter<ExecutionViewFinishedSmall> for ExecutionView {}

impl ExecutionView {
pub fn new(
status: ExecutionStatus,
Expand Down Expand Up @@ -475,7 +484,16 @@ impl ExecutionView {
ExecutionState::Busy => {
self.status = ExecutionStatus::Executing;
}
ExecutionState::Idle => self.status = ExecutionStatus::Finished,
ExecutionState::Idle => {
self.status = ExecutionStatus::Finished;
if self.outputs.is_empty() {
cx.emit(ExecutionViewFinishedEmpty);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While not likely with any of the kernels, if an output comes after an idle then we may be keeping a loose checkmark when it should be cleared out.

} else if ReplSettings::get_global(cx).inline_output {
if let Some(small_text) = self.get_small_inline_output(cx) {
cx.emit(ExecutionViewFinishedSmall(small_text));
}
}
}
ExecutionState::Unknown => self.status = ExecutionStatus::Unknown,
ExecutionState::Starting => self.status = ExecutionStatus::ConnectingToKernel,
ExecutionState::Restarting => self.status = ExecutionStatus::Restarting,
Expand Down Expand Up @@ -527,6 +545,35 @@ impl ExecutionView {
}
}

/// Check if the output is a single small plain text that can be shown inline.
/// Returns the text if it's suitable for inline display (single line, short enough).
fn get_small_inline_output(&self, cx: &App) -> Option<String> {
// Only consider single outputs
if self.outputs.len() != 1 {
return None;
}

let output = self.outputs.first()?;

// Only Plain outputs can be inlined
let content = match output {
Output::Plain { content, .. } => content,
_ => return None,
};

let text = content.read(cx).full_text();
let trimmed = text.trim();

let max_length = ReplSettings::get_global(cx).inline_output_max_length;

// Must be a single line and within the configured max length
if trimmed.contains('\n') || trimmed.len() > max_length {
return None;
}

Some(trimmed.to_string())
}

fn apply_terminal_text(
&mut self,
text: &str,
Expand Down
102 changes: 102 additions & 0 deletions crates/repl/src/repl_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,36 @@ fn runnable_ranges(
}

let snippet_range = cell_range(buffer, range.start.row, range.end.row);

// Check if the snippet range is entirely blank, if so, skip forward to find code
let is_blank =
(snippet_range.start.row..=snippet_range.end.row).all(|row| buffer.is_line_blank(row));

if is_blank {
// Search forward for the next non-blank line
let max_row = buffer.max_point().row;
let mut next_row = snippet_range.end.row + 1;
while next_row <= max_row && buffer.is_line_blank(next_row) {
next_row += 1;
}

if next_row <= max_row {
// Found a non-blank line, find the extent of this cell
let next_snippet_range = cell_range(buffer, next_row, next_row);
let start_language = buffer.language_at(next_snippet_range.start);
let end_language = buffer.language_at(next_snippet_range.end);

if start_language
.zip(end_language)
.is_some_and(|(start, end)| start == end)
{
return (vec![next_snippet_range], None);
}
}

return (Vec::new(), None);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one edge case to deal with, though it's not too big a deal. If you run the last line in a script where there's no newline the checkmark ends up after the line you actually ran.

Image

As an alternative, this could be emitted as a bit of an output "marker" so that if there were display updates that come in after (likely via a callback) then we can toss the output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!


let start_language = buffer.language_at(snippet_range.start);
let end_language = buffer.language_at(snippet_range.end);

Expand Down Expand Up @@ -821,4 +851,76 @@ mod tests {
},]
);
}

#[gpui::test]
fn test_skip_blank_lines_to_next_cell(cx: &mut App) {
let test_language = Arc::new(Language::new(
LanguageConfig {
name: "TestLang".into(),
line_comments: vec!["# ".into()],
..Default::default()
},
None,
));

let buffer = cx.new(|cx| {
Buffer::local(
indoc! { r#"
print(1 + 1)

print(2 + 2)
"# },
cx,
)
.with_language(test_language.clone(), cx)
});
let snapshot = buffer.read(cx).snapshot();

// Selection on blank line should skip to next non-blank cell
let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
.collect::<Vec<_>>();
assert_eq!(snippets, vec!["print(2 + 2)"]);

// Multiple blank lines should also skip forward
let buffer = cx.new(|cx| {
Buffer::local(
indoc! { r#"
print(1 + 1)



print(2 + 2)
"# },
cx,
)
.with_language(test_language.clone(), cx)
});
let snapshot = buffer.read(cx).snapshot();

let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 0)..Point::new(2, 0), cx);
let snippets = snippets
.into_iter()
.map(|range| snapshot.text_for_range(range).collect::<String>())
.collect::<Vec<_>>();
assert_eq!(snippets, vec!["print(2 + 2)"]);

// Blank lines at end of file should return nothing
let buffer = cx.new(|cx| {
Buffer::local(
indoc! { r#"
print(1 + 1)

"# },
cx,
)
.with_language(test_language, cx)
});
let snapshot = buffer.read(cx).snapshot();

let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
assert!(snippets.is_empty());
}
}
11 changes: 11 additions & 0 deletions crates/repl/src/repl_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ pub struct ReplSettings {
///
/// Default: 128
pub max_columns: usize,
/// Whether to show small single-line outputs inline instead of in a block.
///
/// Default: true
pub inline_output: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this now and loving it.

/// Maximum number of characters for an output to be shown inline.
/// Only applies when `inline_output` is true.
///
/// Default: 50
pub inline_output_max_length: usize,
}

impl Settings for ReplSettings {
Expand All @@ -22,6 +31,8 @@ impl Settings for ReplSettings {
Self {
max_lines: repl.max_lines.unwrap(),
max_columns: repl.max_columns.unwrap(),
inline_output: repl.inline_output.unwrap_or(true),
inline_output_max_length: repl.inline_output_max_length.unwrap_or(50),
}
}
}
Loading