Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --readonly flag to enter readonly mode #7128

Closed
wants to merge 13 commits into from
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'upstream/master' into readonly
  • Loading branch information
ds-cbo committed Jul 10, 2023
commit c37fd260686a8e55d0c9f7896f99f64a1ce54eab
321 changes: 1 addition & 320 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,325 +114,6 @@ impl Preview<'_, '_> {
}
}

impl<T: Item + 'static> FilePicker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
let truncate_start = true;
let mut picker = Picker::new(options, editor_data, callback_fn);
picker.truncate_start = truncate_start;

Self {
picker,
truncate_start,
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn),
}
}

pub fn truncate_start(mut self, truncate_start: bool) -> Self {
self.truncate_start = truncate_start;
self.picker.truncate_start = truncate_start;
self
}

fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
self.picker
.selection()
.and_then(|current| (self.file_fn)(editor, current))
.and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line)))
}

/// Get (cached) preview for a given path. If a document corresponding
/// to the path is already open in the editor, it is used instead.
fn get_preview<'picker, 'editor>(
&'picker mut self,
path_or_id: PathOrId,
editor: &'editor Editor,
) -> Preview<'picker, 'editor> {
match path_or_id {
PathOrId::Path(path) => {
let path = &path;
if let Some(doc) = editor.document_by_path(path) {
return Preview::EditorDocument(doc);
}

if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]);
}

let data = std::fs::File::open(path).and_then(|file| {
let metadata = file.metadata()?;
// Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
self.read_buffer.clear();
Ok((metadata, content_type))
});
let preview = data
.map(
|(metadata, content_type)| match (metadata.len(), content_type) {
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
CachedPreview::LargeFile
}
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, None, editor.config.clone(), true)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
)
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
}
PathOrId::Id(id) => {
let doc = editor.documents.get(&id).unwrap();
Preview::EditorDocument(doc)
}
}
}

fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
let Some((current_file, _)) = self.current_file(cx.editor) else {
return EventResult::Consumed(None)
};

// Try to find a document in the cache
let doc = match &current_file {
PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return EventResult::Consumed(None),
},
};

let mut callback: Option<compositor::Callback> = None;

// Then attempt to highlight it if it has no language set
if doc.language_config().is_none() {
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = cx.editor.syn_loader.clone();
let job = tokio::task::spawn_blocking(move || {
let syntax = language_config
.highlight_config(&loader.scopes())
.and_then(|highlight_config| Syntax::new(&text, highlight_config, loader));
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
let Some(syntax) = syntax else {
log::info!("highlighting picker item failed");
return
};
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Self>>() else {
log::info!("picker closed before syntax highlighting finished");
return
};
// Try to find a document in the cache
let doc = match current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return,
},
};
doc.syntax = Some(syntax);
};
Callback::EditorCompositor(Box::new(callback))
});
let tmp: compositor::Callback = Box::new(move |_, ctx| {
ctx.jobs
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
});
callback = Some(Box::new(tmp))
}
}

// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future

EventResult::Consumed(callback)
}
}

impl<T: Item + 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
// +---------+ | |
// |picker | | |
// | | | |
// +---------+ +---------+

let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background);

let picker_width = if render_preview {
area.width / 2
} else {
area.width
};

let picker_area = area.with_width(picker_width);
self.picker.render(picker_area, surface, cx);

if !render_preview {
return;
}

let preview_area = area.clip_left(picker_width);

// don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL);

// calculate the inner area inside the box
let inner = block.inner(preview_area);
// 1 column gap on either side
let margin = Margin::horizontal(1);
let inner = inner.inner(&margin);
block.render(preview_area, surface);

if let Some((path, range)) = self.current_file(cx.editor) {
let preview = self.get_preview(path, cx.editor);
let doc = match preview.document() {
Some(doc) => doc,
None => {
let alt_text = preview.placeholder();
let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
let y = inner.y + inner.height / 2;
surface.set_stringn(x, y, alt_text, inner.width as usize, text);
return;
}
};

// align to middle
let first_line = range
.map(|(start, end)| {
let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2);
middle.saturating_sub(inner.height as usize / 2).min(start)
})
.unwrap_or(0);

let offset = ViewPosition {
anchor: doc.text().line_to_char(first_line),
horizontal_offset: 0,
vertical_offset: 0,
};

let mut highlights = EditorView::doc_syntax_highlights(
doc,
offset.anchor,
area.height,
&cx.editor.theme,
);
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
if spans.is_empty() {
continue;
}
highlights = Box::new(helix_core::syntax::merge(highlights, spans));
}
let mut decorations: Vec<Box<dyn LineDecoration>> = Vec::new();

if let Some((start, end)) = range {
let style = cx
.editor
.theme
.try_get("ui.highlight")
.unwrap_or_else(|| cx.editor.theme.get("ui.selection"));
let draw_highlight = move |renderer: &mut TextRenderer, pos: LinePos| {
if (start..=end).contains(&pos.doc_line) {
let area = Rect::new(
renderer.viewport.x,
renderer.viewport.y + pos.visual_line,
renderer.viewport.width,
1,
);
renderer.surface.set_style(area, style)
}
};
decorations.push(Box::new(draw_highlight))
}

render_document(
surface,
inner,
doc,
offset,
// TODO: compute text annotations asynchronously here (like inlay hints)
&TextAnnotations::default(),
highlights,
&cx.editor.theme,
&mut decorations,
&mut [],
);
}
}

fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
if let Event::IdleTimeout = event {
return self.handle_idle_timeout(ctx);
}
// TODO: keybinds for scrolling preview
self.picker.handle_event(event, ctx)
}

fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.picker.cursor(area, ctx)
}

fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
width / 2
} else {
width
};
self.picker.required_size((picker_width, height))?;
Some((width, height))
}

fn id(&self) -> Option<&'static str> {
Some("file-picker")
}
}

#[derive(PartialEq, Eq, Debug)]
struct PickerMatch {
score: i64,
index: usize,
len: usize,
}

impl PickerMatch {
fn key(&self) -> impl Ord {
(cmp::Reverse(self.score), self.len, self.index)
}
}

impl PartialOrd for PickerMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl Ord for PickerMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.key().cmp(&other.key())
}
}

type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;

pub struct Picker<T: Item> {
options: Vec<T>,
editor_data: T::Data,
Expand Down Expand Up @@ -732,7 +413,7 @@ impl<T: Item + 'static> Picker<T> {
}
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
Document::open(path, None, None, editor.config.clone())
Document::open(path, None, None, editor.config.clone(), false)
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
Expand Down
3 changes: 2 additions & 1 deletion helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,8 @@ impl Document {
}

pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self {
let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
let line_ending: LineEnding = config.load().default_line_ending.into();
let text = Rope::from(line_ending.as_str());
Self::from(text, None, config, false)
}

Expand Down
22 changes: 17 additions & 5 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1397,11 +1397,23 @@ impl Editor {
}

pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
let (rope, encoding, has_bom) = crate::document::from_reader(&mut stdin(), None)?;
Ok(self.new_file_from_document(
action,
Document::from(rope, Some((encoding, has_bom)), self.config.clone(), false),
))
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
let doc = Document::from(
helix_core::Rope::default(),
Some((encoding, has_bom)),
self.config.clone(),
false,
);
let doc_id = self.new_file_from_document(action, doc);
let doc = doc_mut!(self, &doc_id);
let view = view_mut!(self);
doc.ensure_view_init(view.id);
let transaction =
helix_core::Transaction::insert(doc.text(), doc.selection(view.id), stdin.into())
.with_selection(Selection::point(0));
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(doc_id)
}

// ??? possible use for integration tests
Expand Down
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.