Compare commits
1 Commits
master
...
fdb92fbd0e
| Author | SHA1 | Date | |
|---|---|---|---|
| fdb92fbd0e |
58
src/app.rs
58
src/app.rs
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
|
io::Read,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, mpsc},
|
sync::{Arc, mpsc},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
@@ -7,12 +8,11 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command_palette::{self, CommandPalette},
|
|
||||||
file_editor::{FileEditor, SaveStatus},
|
file_editor::{FileEditor, SaveStatus},
|
||||||
folder::Folder,
|
folder::Folder,
|
||||||
preferences::Preferences,
|
preferences::Preferences,
|
||||||
text_styles::{H1, H1_MONO, H2, H2_MONO, H3, H3_MONO, H4, H4_MONO, H5, H5_MONO, H6, H6_MONO},
|
text_styles::{H1, H1_MONO, H2, H2_MONO, H3, H3_MONO, H4, H4_MONO, H5, H5_MONO, H6, H6_MONO},
|
||||||
util::{GuiSender, log_error},
|
util::{GuiSender, file_mtime, log_error},
|
||||||
};
|
};
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Context, FontData, FontDefinitions, FontFamily, FontId, Frame, Image, Key,
|
Align, Button, Context, FontData, FontDefinitions, FontFamily, FontId, Frame, Image, Key,
|
||||||
@@ -40,9 +40,6 @@ pub struct App {
|
|||||||
open_tab_index: Option<usize>,
|
open_tab_index: Option<usize>,
|
||||||
|
|
||||||
next_tab_id: TabId,
|
next_tab_id: TabId,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
command_palette: Option<CommandPalette>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Jobs {
|
pub struct Jobs {
|
||||||
@@ -132,7 +129,6 @@ impl Default for App {
|
|||||||
next_tab_id: 2,
|
next_tab_id: 2,
|
||||||
show_folders: false,
|
show_folders: false,
|
||||||
folders: vec![],
|
folders: vec![],
|
||||||
command_palette: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,10 +292,6 @@ impl eframe::App for App {
|
|||||||
if input.consume_key(Modifiers::CTRL, Key::S) {
|
if input.consume_key(Modifiers::CTRL, Key::S) {
|
||||||
self.save_active_tab(ctx);
|
self.save_active_tab(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.consume_key(Modifiers::CTRL, Key::K) {
|
|
||||||
self.command_palette = Some(Default::default());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
@@ -318,10 +310,19 @@ impl eframe::App for App {
|
|||||||
self.jobs.start(ui.ctx(), move || {
|
self.jobs.start(ui.ctx(), move || {
|
||||||
let file_path = rfd::FileDialog::new().pick_file()?;
|
let file_path = rfd::FileDialog::new().pick_file()?;
|
||||||
|
|
||||||
log_error(eyre!("Failed to open file {file_path:?}"), || {
|
let mut file = fs::File::open(&file_path)
|
||||||
FileEditor::open_file(file_path)
|
.inspect_err(|e| log::error!("Failed to open {file_path:?}: {e}"))
|
||||||
})
|
.ok()?;
|
||||||
.map(Action::OpenFile)
|
|
||||||
|
let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?;
|
||||||
|
|
||||||
|
let mut text = String::new();
|
||||||
|
file.read_to_string(&mut text)
|
||||||
|
.inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}"))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let editor = FileEditor::from_file(file_path, &text, mtime);
|
||||||
|
Some(Action::OpenFile(editor))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,10 +445,23 @@ impl eframe::App for App {
|
|||||||
if let Some(file_path) = response.open_file {
|
if let Some(file_path) = response.open_file {
|
||||||
let file_path = file_path.to_owned();
|
let file_path = file_path.to_owned();
|
||||||
self.jobs.start(ui.ctx(), move || {
|
self.jobs.start(ui.ctx(), move || {
|
||||||
log_error(eyre!("Failed to open file {file_path:?}"), || {
|
let mut file = fs::File::open(&file_path)
|
||||||
FileEditor::open_file(file_path)
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to open {file_path:?}: {e}")
|
||||||
})
|
})
|
||||||
.map(Action::OpenFile)
|
.ok()?;
|
||||||
|
|
||||||
|
let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?;
|
||||||
|
|
||||||
|
let mut text = String::new();
|
||||||
|
file.read_to_string(&mut text)
|
||||||
|
.inspect_err(|e| {
|
||||||
|
log::error!("Failed to read {file_path:?}: {e}")
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let editor = FileEditor::from_file(file_path, &text, mtime);
|
||||||
|
Some(Action::OpenFile(editor))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,16 +489,6 @@ impl eframe::App for App {
|
|||||||
egui::warn_if_debug_build(ui);
|
egui::warn_if_debug_build(ui);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(command_palette) = &mut self.command_palette {
|
|
||||||
match command_palette.show(ctx, &mut self.jobs, &mut self.folders) {
|
|
||||||
command_palette::Response::None => {}
|
|
||||||
command_palette::Response::Close => {
|
|
||||||
self.command_palette = None;
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cmp::min,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use egui::{Key, Modifiers, RichText, Ui, Window};
|
|
||||||
use eyre::eyre;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
app::{Action, Jobs},
|
|
||||||
file_editor::FileEditor,
|
|
||||||
folder::Folder,
|
|
||||||
util::log_error,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct CommandPalette {
|
|
||||||
first_frame: bool,
|
|
||||||
input: String,
|
|
||||||
results: Vec<OpenFileCommand>,
|
|
||||||
list_i: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CommandPalette {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
first_frame: true,
|
|
||||||
input: Default::default(),
|
|
||||||
results: Default::default(),
|
|
||||||
list_i: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OpenFileCommand {
|
|
||||||
pretty: String,
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Response {
|
|
||||||
None,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandPalette {
|
|
||||||
pub fn show(
|
|
||||||
&mut self,
|
|
||||||
ctx: &egui::Context,
|
|
||||||
jobs: &mut Jobs,
|
|
||||||
folders: &mut [Folder],
|
|
||||||
) -> Response {
|
|
||||||
Window::new("Command Palette")
|
|
||||||
.collapsible(false)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
let text_edit = ui.text_edit_singleline(&mut self.input);
|
|
||||||
let mut response = Response::None;
|
|
||||||
if self.first_frame {
|
|
||||||
self.first_frame = false;
|
|
||||||
text_edit.request_focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if text_edit.has_focus() || text_edit.lost_focus() {
|
|
||||||
ui.input_mut(|input| {
|
|
||||||
if input.consume_key(Modifiers::NONE, Key::ArrowUp) {
|
|
||||||
self.list_i = self.list_i.saturating_sub(1);
|
|
||||||
};
|
|
||||||
if input.consume_key(Modifiers::NONE, Key::ArrowDown) {
|
|
||||||
self.list_i =
|
|
||||||
min(self.list_i + 1, self.results.len().saturating_sub(1));
|
|
||||||
};
|
|
||||||
if input.consume_key(Modifiers::NONE, Key::Enter)
|
|
||||||
&& let Some(result) = self.results.get(self.list_i)
|
|
||||||
{
|
|
||||||
let path = result.path.clone();
|
|
||||||
log::info!("open {}", result.pretty);
|
|
||||||
jobs.start(ui.ctx(), move || {
|
|
||||||
log_error(eyre!("Failed to open file {path:?}"), move || {
|
|
||||||
FileEditor::open_file(path)
|
|
||||||
})
|
|
||||||
.map(Action::OpenFile)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !text_edit.has_focus() {
|
|
||||||
log::info!("sad face");
|
|
||||||
response = Response::Close;
|
|
||||||
}
|
|
||||||
|
|
||||||
if text_edit.changed() {
|
|
||||||
self.results.clear();
|
|
||||||
|
|
||||||
if !self.input.is_empty() {
|
|
||||||
self.input = self.input.to_lowercase();
|
|
||||||
search_files(ui, &self.input, "".as_ref(), folders, &mut self.results);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list_i = min(self.list_i, self.results.len().saturating_sub(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, result) in self.results.iter().enumerate() {
|
|
||||||
let highlight = i == self.list_i;
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let mut label = RichText::new("> open");
|
|
||||||
if highlight {
|
|
||||||
label = label.strong().underline();
|
|
||||||
}
|
|
||||||
ui.label(label);
|
|
||||||
ui.monospace(&result.pretty);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
})
|
|
||||||
.expect("Window is always open")
|
|
||||||
.inner
|
|
||||||
.expect("Windows is never collapsed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search the tree of [Folder]s for files matching `query`.
|
|
||||||
fn search_files(
|
|
||||||
ui: &mut Ui,
|
|
||||||
query: &str,
|
|
||||||
path: &Path,
|
|
||||||
folders: &mut [Folder],
|
|
||||||
out: &mut Vec<OpenFileCommand>,
|
|
||||||
) {
|
|
||||||
for folder in folders {
|
|
||||||
let Some(folder) = folder.load(ui) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = path.join(&folder.name);
|
|
||||||
|
|
||||||
for file in &folder.child_files {
|
|
||||||
if file.name.to_lowercase().contains(query) {
|
|
||||||
let pretty = path.join(&file.name).to_string_lossy().to_string();
|
|
||||||
let path = file.path.clone();
|
|
||||||
out.push(OpenFileCommand { pretty, path });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
search_files(ui, query, &path, &mut folder.child_folders, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ use std::{
|
|||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{Read as _, Write},
|
io::Write,
|
||||||
ops::{Div as _, Sub as _},
|
ops::{Div as _, Sub as _},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
@@ -13,7 +13,7 @@ use chrono::{DateTime, Local};
|
|||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, vec2,
|
Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, vec2,
|
||||||
};
|
};
|
||||||
use eyre::{Context as _, eyre};
|
use eyre::eyre;
|
||||||
use notify::{EventKind, Watcher};
|
use notify::{EventKind, Watcher};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -100,7 +100,7 @@ impl FileEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path(file_path: PathBuf, contents: &str, mtime: DateTime<Local>) -> Self {
|
pub fn from_file(file_path: PathBuf, contents: &str, mtime: DateTime<Local>) -> Self {
|
||||||
let file_title = file_path
|
let file_title = file_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|name| name.to_string_lossy().to_string())
|
.map(|name| name.to_string_lossy().to_string())
|
||||||
@@ -115,19 +115,6 @@ impl FileEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_file(file_path: PathBuf) -> eyre::Result<Self> {
|
|
||||||
let mut file =
|
|
||||||
fs::File::open(&file_path).wrap_err_with(|| eyre!("Failed to open {file_path:?}"))?;
|
|
||||||
|
|
||||||
let mtime = file_mtime(&file)?;
|
|
||||||
|
|
||||||
let mut text = String::new();
|
|
||||||
file.read_to_string(&mut text)
|
|
||||||
.wrap_err_with(|| eyre!("Failed to read {file_path:?}"))?;
|
|
||||||
|
|
||||||
Ok(FileEditor::from_path(file_path, &text, mtime))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title(&self) -> &str {
|
pub fn title(&self) -> &str {
|
||||||
&self.title
|
&self.title
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ impl LoadedFolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Folder {
|
impl Folder {
|
||||||
pub fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
||||||
if let Folder::NotLoaded { name, path } = self {
|
if let Folder::NotLoaded { name, path } = self {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use zerocopy::{FromBytes, IntoBytes};
|
|||||||
use crate::{custom_code_block::try_from_custom_code_block, rasterizer};
|
use crate::{custom_code_block::try_from_custom_code_block, rasterizer};
|
||||||
use crate::{custom_code_block::write_custom_code_block, util::random_id};
|
use crate::{custom_code_block::write_custom_code_block, util::random_id};
|
||||||
|
|
||||||
use self::tool::{Tool, ToolEvent};
|
use self::tool::{ToolEvent, Tool};
|
||||||
|
|
||||||
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
|
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
|
||||||
const HANDWRITING_BOTTOM_PADDING: f32 = 80.0;
|
const HANDWRITING_BOTTOM_PADDING: f32 = 80.0;
|
||||||
@@ -89,6 +89,9 @@ struct Ephemeral {
|
|||||||
/// Tool position last frame, in canvas space.
|
/// Tool position last frame, in canvas space.
|
||||||
last_tool_position: Option<Pos2>,
|
last_tool_position: Option<Pos2>,
|
||||||
|
|
||||||
|
/// Whether the handwriting element is being interacted with.
|
||||||
|
is_focused: bool,
|
||||||
|
|
||||||
/// The stroke that is currently being drawed.
|
/// The stroke that is currently being drawed.
|
||||||
current_stroke: Vec<Pos2>,
|
current_stroke: Vec<Pos2>,
|
||||||
|
|
||||||
@@ -139,9 +142,10 @@ impl Default for Ephemeral {
|
|||||||
Self {
|
Self {
|
||||||
id: random_id(),
|
id: random_id(),
|
||||||
canvas_rasterizer: Default::default(),
|
canvas_rasterizer: Default::default(),
|
||||||
tool: Tool::Pencil,
|
tool: Tool::Eraser,
|
||||||
tool_position: None,
|
tool_position: None,
|
||||||
last_tool_position: None,
|
last_tool_position: None,
|
||||||
|
is_focused: false,
|
||||||
current_stroke: Default::default(),
|
current_stroke: Default::default(),
|
||||||
tessellator: None,
|
tessellator: None,
|
||||||
mesh: Default::default(),
|
mesh: Default::default(),
|
||||||
@@ -238,9 +242,16 @@ impl Handwriting {
|
|||||||
// Was the user in the process of drawing a stroke last frame?
|
// Was the user in the process of drawing a stroke last frame?
|
||||||
let was_drawing = !self.e.current_stroke.is_empty();
|
let was_drawing = !self.e.current_stroke.is_empty();
|
||||||
|
|
||||||
// Is the user in the process of drawing a stroke now?
|
// Is the user in the process of drawing a stroke now?;
|
||||||
let is_drawing = response.interact_pointer_pos().is_some();
|
let is_drawing = response.interact_pointer_pos().is_some();
|
||||||
|
|
||||||
|
if is_drawing {
|
||||||
|
self.e.is_focused = true;
|
||||||
|
} else if ui.ctx().input(|i| i.pointer.any_down()) {
|
||||||
|
// if the user starts interactive with something else, unfocus the handwriting area.
|
||||||
|
self.e.is_focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
if !is_drawing {
|
if !is_drawing {
|
||||||
if was_drawing {
|
if was_drawing {
|
||||||
// commit current line
|
// commit current line
|
||||||
@@ -248,12 +259,18 @@ impl Handwriting {
|
|||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let padding = if self.e.is_focused {
|
||||||
|
HANDWRITING_BOTTOM_PADDING
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
// recalculate how tall the widget should be
|
// recalculate how tall the widget should be
|
||||||
let lines_max_y = self
|
let lines_max_y = self
|
||||||
.strokes
|
.strokes
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|p| p.y + HANDWRITING_BOTTOM_PADDING)
|
.map(|p| p.y + padding)
|
||||||
.fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y));
|
.fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y));
|
||||||
|
|
||||||
// Change the height of the handwriting item.
|
// Change the height of the handwriting item.
|
||||||
@@ -299,7 +316,9 @@ impl Handwriting {
|
|||||||
self.e.tool_position = tool_event.position();
|
self.e.tool_position = tool_event.position();
|
||||||
match self.e.tool {
|
match self.e.tool {
|
||||||
Tool::Pencil => {
|
Tool::Pencil => {
|
||||||
hw_response.changed |= tool::pencil::on_tool_event(self, tool_event);
|
hw_response.changed |=
|
||||||
|
tool::pencil::on_tool_event(self, tool_event);
|
||||||
|
|
||||||
}
|
}
|
||||||
Tool::Eraser => {
|
Tool::Eraser => {
|
||||||
if tool::eraser::on_tool_event(self, tool_event) {
|
if tool::eraser::on_tool_event(self, tool_event) {
|
||||||
@@ -365,9 +384,7 @@ impl Handwriting {
|
|||||||
// Draw the texture
|
// Draw the texture
|
||||||
self.e.canvas_rasterizer.show(ui.ctx(), &painter, mesh_rect);
|
self.e.canvas_rasterizer.show(ui.ctx(), &painter, mesh_rect);
|
||||||
|
|
||||||
if let Some(tool_position) = self.e.tool_position
|
if let Some(tool_position) = self.e.tool_position && let Tool::Eraser = self.e.tool {
|
||||||
&& let Tool::Eraser = self.e.tool
|
|
||||||
{
|
|
||||||
let pos = to_screen * tool_position;
|
let pos = to_screen * tool_position;
|
||||||
let shape = Shape::circle_stroke(pos, tool::eraser::RADIUS, style.stroke);
|
let shape = Shape::circle_stroke(pos, tool::eraser::RADIUS, style.stroke);
|
||||||
painter.add(shape);
|
painter.add(shape);
|
||||||
@@ -780,8 +797,6 @@ mod test {
|
|||||||
|
|
||||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, emath::RectTransform};
|
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, emath::RectTransform};
|
||||||
|
|
||||||
use crate::handwriting::tool::pencil;
|
|
||||||
|
|
||||||
use super::{Handwriting, process_event};
|
use super::{Handwriting, process_event};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -980,7 +995,7 @@ mod test {
|
|||||||
let from_screen = from_screen();
|
let from_screen = from_screen();
|
||||||
for event in TEST_EVENTS {
|
for event in TEST_EVENTS {
|
||||||
process_event(&mut last_pos, from_screen, event, |tool_event| {
|
process_event(&mut last_pos, from_screen, event, |tool_event| {
|
||||||
pencil::on_tool_event(&mut handwriting, tool_event);
|
handwriting.on_tool_event(tool_event);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let serialized = handwriting.to_string();
|
let serialized = handwriting.to_string();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod command_palette;
|
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod custom_code_block;
|
pub mod custom_code_block;
|
||||||
pub mod file_editor;
|
pub mod file_editor;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::iter;
|
use std::iter::{self, once};
|
||||||
|
|
||||||
use crate::markdown::Style;
|
use crate::markdown::Style;
|
||||||
|
|
||||||
@@ -142,8 +142,18 @@ fn collect_until<'a, const N: usize>(
|
|||||||
first_token: Option<&Token<'a>>,
|
first_token: Option<&Token<'a>>,
|
||||||
tokens: &mut &[Token<'a>],
|
tokens: &mut &[Token<'a>],
|
||||||
pattern: impl FnMut(&[Token<'a>; N]) -> bool,
|
pattern: impl FnMut(&[Token<'a>; N]) -> bool,
|
||||||
) -> Span<'a> {
|
) -> Span<'a>
|
||||||
let split_at = match tokens.array_windows::<N>().position(pattern) {
|
where
|
||||||
|
// &[T; N]: TryFrom<&[T]>
|
||||||
|
for<'b> &'b [Token<'a>; N]: TryFrom<&'b [Token<'a>]>,
|
||||||
|
{
|
||||||
|
let mut windows = tokens.windows(N).map(|slice| {
|
||||||
|
<&[Token<'a>; N]>::try_from(slice)
|
||||||
|
.ok()
|
||||||
|
.expect("`windows` promises to return slices of length N")
|
||||||
|
});
|
||||||
|
|
||||||
|
let split_at = match windows.position(pattern) {
|
||||||
Some(i) => i + N,
|
Some(i) => i + N,
|
||||||
None => tokens.len(), // consume everything
|
None => tokens.len(), // consume everything
|
||||||
};
|
};
|
||||||
@@ -151,8 +161,8 @@ fn collect_until<'a, const N: usize>(
|
|||||||
let (consume, keep) = tokens.split_at(split_at);
|
let (consume, keep) = tokens.split_at(split_at);
|
||||||
*tokens = keep;
|
*tokens = keep;
|
||||||
|
|
||||||
first_token
|
once(first_token)
|
||||||
.into_iter()
|
.flatten()
|
||||||
.chain(consume)
|
.chain(consume)
|
||||||
.fold(Span::empty(), |span: Span<'_>, token| {
|
.fold(Span::empty(), |span: Span<'_>, token| {
|
||||||
span.try_merge(&token.span).unwrap()
|
span.try_merge(&token.span).unwrap()
|
||||||
|
|||||||
Reference in New Issue
Block a user