Added Todo syntax highlighter
This commit is contained in:
Generated
+510
-492
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -7,7 +7,8 @@ description = "A COSMIC todo.txt application"
|
||||
repository = "https://github.com/professorwaltwood/planit.git"
|
||||
|
||||
[dependencies]
|
||||
cosmic-files = { git = "https://github.com/pop-os/cosmic-files.git", version = "1.0.8" }
|
||||
cosmic-files = { git = "https://github.com/pop-os/cosmic-files", version = "1.0.8" }
|
||||
# cosmic-files = { git = "https://github.com/pop-os/cosmic-files.git", version = "1.0.8" }
|
||||
futures-util = "0.3.31"
|
||||
i18n-embed = { version = "0.16", features = [
|
||||
"fluent-system",
|
||||
@@ -17,6 +18,7 @@ i18n-embed-fl = "0.10"
|
||||
log = "0.4.29"
|
||||
open = "5.3.2"
|
||||
rust-embed = "8.8.0"
|
||||
syntect = "5.3.0"
|
||||
time = { version = "0.3.47", features = ["macros", "parsing"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
|
||||
@@ -26,6 +28,7 @@ git = "https://github.com/pop-os/libcosmic.git"
|
||||
features = [
|
||||
# Accessibility support
|
||||
#"a11y",
|
||||
"debug",
|
||||
# About widget for the app
|
||||
"about",
|
||||
# Uses cosmic-settings-daemon to watch for config file changes
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
%YAML 1.2
|
||||
---
|
||||
name: todo.txt
|
||||
scope: text.todo
|
||||
variables:
|
||||
date: '(\d{4}-\d{2}-\d{2}(?:\s+|$))'
|
||||
prio: '(\([A-Z]\)(?:\s+|$))'
|
||||
contexts:
|
||||
main:
|
||||
- match: '^\s*x\s'
|
||||
push: done
|
||||
|
||||
- match: ""
|
||||
push: todo
|
||||
|
||||
done:
|
||||
- meta_scope: comment.line
|
||||
- include: check_prio
|
||||
|
||||
todo:
|
||||
- meta_scope: text.todo
|
||||
- include: check_prio
|
||||
|
||||
desc_fallback:
|
||||
- match: '\s'
|
||||
- match: ""
|
||||
push: desc_start
|
||||
|
||||
check_prio:
|
||||
- match: "{{date}}"
|
||||
scope: constant.numeric
|
||||
push: check_date2
|
||||
|
||||
- match: "{{prio}}"
|
||||
scope: keyword
|
||||
push: check_date
|
||||
|
||||
- include: desc_fallback
|
||||
|
||||
check_date:
|
||||
- match: "{{date}}"
|
||||
scope: constant.numeric
|
||||
set: check_date2
|
||||
- include: desc_fallback
|
||||
|
||||
check_date2:
|
||||
- match: "{{date}}"
|
||||
scope: constant.numeric
|
||||
set: desc_start
|
||||
- include: desc_fallback
|
||||
|
||||
# Any preceeding whitespace has been read
|
||||
# safe to assume start of match is succeeds whitespace
|
||||
desc_start:
|
||||
- match: '@\S+'
|
||||
scope: string
|
||||
push: desc
|
||||
- match: '\+\S+'
|
||||
scope: variable.function
|
||||
push: desc
|
||||
- match: '\b([^\s:]+):([^\s:]+)\b'
|
||||
captures:
|
||||
1: entity.name
|
||||
2: entity.name.tag
|
||||
push: desc
|
||||
- match: ""
|
||||
set: desc
|
||||
|
||||
desc:
|
||||
- match: '(?:\s|^)@\S+'
|
||||
scope: string
|
||||
- match: '(?:\s|^)\+\S+'
|
||||
scope: variable.function
|
||||
- match: '\b([^\s:]+):([^\s:]+)\b'
|
||||
captures:
|
||||
1: entity.name
|
||||
2: entity.name.tag
|
||||
@@ -18,3 +18,5 @@ test having lots of items +planit @tests
|
||||
test having lots of items +planit @tests
|
||||
test having lots of items +planit @tests
|
||||
test having lots of items +planit @tests
|
||||
+sda sda sda@ asd @Sasd @dsa a@adsa
|
||||
2020-01-01 @asda +asd
|
||||
+50
-18
@@ -2,13 +2,16 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::fl;
|
||||
use crate::todo_editor::TodoHighlighter;
|
||||
use cosmic::app::context_drawer;
|
||||
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
||||
use cosmic::cosmic_config::{self, ConfigSet, CosmicConfigEntry};
|
||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||
use cosmic::iced::futures::channel::mpsc::Sender;
|
||||
use cosmic::iced::{Alignment, Length, Subscription};
|
||||
use cosmic::iced::{Alignment, Length, Subscription, stream};
|
||||
use cosmic::prelude::*;
|
||||
use cosmic::widget::text_editor::{self, Content};
|
||||
use cosmic::widget::{self, about::About, menu};
|
||||
use cosmic::{Theme, iced_futures, prelude::*};
|
||||
|
||||
use futures_util::SinkExt;
|
||||
use std::collections::HashMap;
|
||||
use std::path;
|
||||
@@ -33,6 +36,7 @@ pub struct AppModel {
|
||||
/// Key bindings for the application's menu bar.
|
||||
key_binds: HashMap<menu::KeyBind, MenuAction>,
|
||||
/// Configuration data that persists between application runs.
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
/// Time active
|
||||
time: u32,
|
||||
@@ -41,6 +45,7 @@ pub struct AppModel {
|
||||
/// Current Todo data
|
||||
todo: Option<Todo>,
|
||||
dialog_opt: Option<Dialog<Message>>,
|
||||
content: Content,
|
||||
}
|
||||
|
||||
/// Messages emitted by the application and its widgets.
|
||||
@@ -53,6 +58,7 @@ pub enum Message {
|
||||
OpenFileDialog,
|
||||
DialogMessage(DialogMessage),
|
||||
OpenFileResult(DialogResult),
|
||||
Edit(text_editor::Action),
|
||||
}
|
||||
|
||||
/// Create a COSMIC application from the app model
|
||||
@@ -90,6 +96,26 @@ impl cosmic::Application for AppModel {
|
||||
.links([(fl!("repository"), REPOSITORY)])
|
||||
.license(env!("CARGO_PKG_LICENSE"));
|
||||
|
||||
let (config_handler, config) =
|
||||
match cosmic_config::Config::new(Self::APP_ID, Config::VERSION) {
|
||||
Ok(config_handler) => {
|
||||
let config = match Config::get_entry(&config_handler) {
|
||||
Ok(ok) => ok,
|
||||
Err((errs, config)) => {
|
||||
log::info!("errors loading config: {errs:?}");
|
||||
config
|
||||
}
|
||||
};
|
||||
(Some(config_handler), config)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to create config handler: {err}");
|
||||
(None, Config::default())
|
||||
}
|
||||
};
|
||||
|
||||
let todo = config.current_file.clone().map(|path| Todo::new(path));
|
||||
|
||||
// Construct the app model with the runtime's core.
|
||||
let mut app = AppModel {
|
||||
core,
|
||||
@@ -97,22 +123,13 @@ impl cosmic::Application for AppModel {
|
||||
about,
|
||||
key_binds: HashMap::new(),
|
||||
// Optional configuration file for an application.
|
||||
config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION)
|
||||
.map(|context| match Config::get_entry(&context) {
|
||||
Ok(config) => config,
|
||||
Err((_errors, config)) => {
|
||||
// for why in errors {
|
||||
// tracing::error!(%why, "error loading app config");
|
||||
// }
|
||||
|
||||
config
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
config_handler: config_handler,
|
||||
config: config,
|
||||
time: 0,
|
||||
watch_is_active: false,
|
||||
todo: None,
|
||||
todo: todo,
|
||||
dialog_opt: None,
|
||||
content: Content::with_text("test"),
|
||||
};
|
||||
|
||||
// Create a startup command that sets the window title.
|
||||
@@ -167,6 +184,11 @@ impl cosmic::Application for AppModel {
|
||||
|
||||
let header = widget::row::with_capacity(2)
|
||||
.push(widget::text::title1(fl!("welcome")))
|
||||
.push(
|
||||
widget::TextEditor::new(&self.content)
|
||||
.on_action(Message::Edit)
|
||||
.highlight_with::<TodoHighlighter>((), |hl, _t| hl.to_format()),
|
||||
)
|
||||
.align_y(Alignment::End)
|
||||
.spacing(space_s);
|
||||
|
||||
@@ -222,7 +244,7 @@ impl cosmic::Application for AppModel {
|
||||
// Conditionally enables a timer that emits a message every second.
|
||||
if self.watch_is_active {
|
||||
subscriptions.push(Subscription::run(|| {
|
||||
iced_futures::stream::channel(1, |mut emitter: Sender<Message>| async move {
|
||||
stream::channel(1, |mut emitter: Sender<Message>| async move {
|
||||
let mut time = 1;
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||
|
||||
@@ -295,7 +317,14 @@ impl cosmic::Application for AppModel {
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut todo = Todo::new(cpath);
|
||||
let mut todo = Todo::new(cpath.clone());
|
||||
|
||||
if let Some(cfg) = &self.config_handler {
|
||||
if let Err(err) = cfg.set("current_file", Some(cpath)) {
|
||||
log::error!("failed to save config {:?}", err);
|
||||
}
|
||||
}
|
||||
// self.config.current_file = Some(cpath);
|
||||
// Todo: this might need to be more async
|
||||
if let Err(err) = todo.load() {
|
||||
log::error!("failed to load file {:?} {:?}", path, err);
|
||||
@@ -306,6 +335,9 @@ impl cosmic::Application for AppModel {
|
||||
}
|
||||
DialogResult::Cancel => todo!(),
|
||||
},
|
||||
Message::Edit(action) => {
|
||||
self.content.perform(action);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cosmic::cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
|
||||
|
||||
#[derive(Debug, Default, Clone, CosmicConfigEntry, Eq, PartialEq)]
|
||||
#[version = 1]
|
||||
pub struct Config {
|
||||
demo: String,
|
||||
pub current_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ mod app;
|
||||
mod config;
|
||||
mod i18n;
|
||||
mod todo;
|
||||
mod todo_editor;
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
// Get the system's preferred languages.
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
use cosmic::{
|
||||
iced::core::text::highlighter::{Format, Highlighter},
|
||||
iced::{Color, Font},
|
||||
theme::CosmicColor,
|
||||
};
|
||||
use std::{str::FromStr, sync::LazyLock};
|
||||
use syntect::{highlighting, parsing};
|
||||
|
||||
static TODOSYNTAX: LazyLock<parsing::SyntaxSet> = LazyLock::new(|| {
|
||||
let mut builder = parsing::SyntaxSetBuilder::new();
|
||||
builder.add(
|
||||
parsing::SyntaxDefinition::load_from_str(
|
||||
std::include_str!("../resources/todo.sublime-syntax"),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("Failed to load todo syntax"),
|
||||
);
|
||||
builder.build()
|
||||
});
|
||||
|
||||
static TODOTHEME: LazyLock<highlighting::Theme> = LazyLock::new(|| get_current_theme());
|
||||
|
||||
pub struct TodoHighlighter {
|
||||
syntax: &'static parsing::SyntaxReference,
|
||||
highlighter: highlighting::Highlighter<'static>,
|
||||
}
|
||||
|
||||
impl Highlighter for TodoHighlighter {
|
||||
type Settings = ();
|
||||
|
||||
type Highlight = TodoHighlight;
|
||||
|
||||
type Iterator<'a> = Box<dyn Iterator<Item = (std::ops::Range<usize>, Self::Highlight)> + 'a>;
|
||||
|
||||
fn new(_settings: &Self::Settings) -> Self {
|
||||
let syntax = TODOSYNTAX.syntaxes().get(0).expect("Todo Syntax Reference");
|
||||
let highlighter = highlighting::Highlighter::new(&*TODOTHEME);
|
||||
Self {
|
||||
syntax,
|
||||
highlighter,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _s: &Self::Settings) {
|
||||
// Re-highlight (only one line for planit)
|
||||
self.change_line(0);
|
||||
}
|
||||
|
||||
fn change_line(&mut self, line: usize) {
|
||||
// planit only edits one todo at a time
|
||||
assert_eq!(line, 0);
|
||||
|
||||
// Nothing to do as no point in caching one line
|
||||
}
|
||||
|
||||
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
||||
let mut parser = parsing::ParseState::new(self.syntax);
|
||||
|
||||
let ops = parser.parse_line(line, &*TODOSYNTAX).unwrap_or_default();
|
||||
|
||||
Box::new(scope_iterator(ops, line, &self.highlighter))
|
||||
}
|
||||
|
||||
fn current_line(&self) -> usize {
|
||||
0 // only one line
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TodoHighlight(highlighting::StyleModifier);
|
||||
|
||||
impl TodoHighlight {
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.0.foreground.map(|color| Color {
|
||||
r: color.r as f32 / 255.,
|
||||
g: color.g as f32 / 255.,
|
||||
b: color.b as f32 / 255.,
|
||||
a: color.a as f32 / 255.,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn font(&self) -> Option<Font> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn to_format(&self) -> Format<Font> {
|
||||
Format {
|
||||
color: self.color(),
|
||||
font: self.font(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_iterator<'a>(
|
||||
ops: Vec<(usize, parsing::ScopeStackOp)>,
|
||||
line: &str,
|
||||
highlighter: &'a highlighting::Highlighter<'static>,
|
||||
) -> impl Iterator<Item = (std::ops::Range<usize>, TodoHighlight)> + 'a {
|
||||
let mut stack = parsing::ScopeStack::new();
|
||||
ScopeRangeIterator {
|
||||
ops,
|
||||
line_length: line.len(),
|
||||
index: 0,
|
||||
last_str_index: 0,
|
||||
}
|
||||
.filter_map(move |(range, scope)| {
|
||||
let _ = stack.apply(&scope);
|
||||
|
||||
if range.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
range,
|
||||
TodoHighlight(highlighter.style_mod_for_stack(&stack.scopes)),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct ScopeRangeIterator {
|
||||
ops: Vec<(usize, parsing::ScopeStackOp)>,
|
||||
line_length: usize,
|
||||
index: usize,
|
||||
last_str_index: usize,
|
||||
}
|
||||
|
||||
impl Iterator for ScopeRangeIterator {
|
||||
type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index > self.ops.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let next_str_i = if self.index == self.ops.len() {
|
||||
self.line_length
|
||||
} else {
|
||||
self.ops[self.index].0
|
||||
};
|
||||
|
||||
let range = self.last_str_index..next_str_i;
|
||||
self.last_str_index = next_str_i;
|
||||
|
||||
let op = if self.index == 0 {
|
||||
parsing::ScopeStackOp::Noop
|
||||
} else {
|
||||
self.ops[self.index - 1].1.clone()
|
||||
};
|
||||
|
||||
self.index += 1;
|
||||
Some((range, op))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_theme() -> highlighting::Theme {
|
||||
let theme = cosmic::theme::active();
|
||||
let cosmic = theme.cosmic();
|
||||
|
||||
highlighting::Theme {
|
||||
name: None,
|
||||
author: None,
|
||||
settings: highlighting::ThemeSettings::default(),
|
||||
scopes: vec![
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str("constant.numeric")
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_purple)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str("comment.line")
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.ext_warm_grey)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str("text.todo string - comment.line")
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_green)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str(
|
||||
"text.todo variable.function - comment.line",
|
||||
)
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_blue)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str(
|
||||
"text.todo entity.name - comment.line",
|
||||
)
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_orange)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str(
|
||||
"text.todo entity.name.tag - comment.line",
|
||||
)
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_orange)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
highlighting::ThemeItem {
|
||||
scope: highlighting::ScopeSelectors::from_str("text.todo keyword - comment.line")
|
||||
.expect("static scope selector"),
|
||||
style: highlighting::StyleModifier {
|
||||
foreground: Some(colour_map(cosmic.palette.accent_pink)),
|
||||
background: None,
|
||||
font_style: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn colour_map(colour: CosmicColor) -> highlighting::Color {
|
||||
highlighting::Color {
|
||||
r: (colour.red * 255.) as u8,
|
||||
g: (colour.green * 255.) as u8,
|
||||
b: (colour.blue * 255.) as u8,
|
||||
a: (colour.alpha * 255.) as u8,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user