Skip to content

Commit

Permalink
Improve CSS error handling by parsing GTK error output (see #446)
Browse files Browse the repository at this point in the history
  • Loading branch information
elkowar committed Sep 4, 2022
1 parent 58907ea commit 7623e7e
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 198 deletions.
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 34 additions & 7 deletions crates/eww/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use crate::{
*,
};
use anyhow::anyhow;
use eww_shared_util::VarName;
use codespan_reporting::files::Files;
use eww_shared_util::{Span, VarName};
use glib::ObjectExt;
use itertools::Itertools;
use once_cell::sync::Lazy;
use simplexpr::dynval::DynVal;
use std::{
cell::RefCell,
Expand All @@ -26,6 +28,8 @@ use yuck::{
window_definition::WindowDefinition,
window_geometry::{AnchorPoint, WindowGeometry},
},
error::DiagError,
gen_diagnostic,
value::Coords,
};

Expand Down Expand Up @@ -145,9 +149,15 @@ impl App {
if let Err(e) = config_result.and_then(|new_config| self.load_config(new_config)) {
errors.push(e)
}
let css_result = crate::config::scss::parse_scss_from_file(&self.paths.get_eww_scss_path());
if let Err(e) = css_result.and_then(|css| self.load_css(&css)) {
errors.push(e)
match crate::config::scss::parse_scss_from_file(&self.paths.get_eww_scss_path()) {
Ok((file_id, css)) => {
if let Err(e) = self.load_css(file_id, &css) {
errors.push(anyhow!(e));
}
}
Err(e) => {
errors.push(e);
}
}

sender.respond_with_error_list(errors)?;
Expand Down Expand Up @@ -400,9 +410,26 @@ impl App {
Ok(())
}

pub fn load_css(&mut self, css: &str) -> Result<()> {
self.css_provider.load_from_data(css.as_bytes())?;
Ok(())
/// Load a given CSS string into the gtk css provider, returning a nicely formatted [`DiagError`] when GTK errors out
pub fn load_css(&mut self, file_id: usize, css: &str) -> Result<()> {
if let Err(err) = self.css_provider.load_from_data(css.as_bytes()) {
static PATTERN: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"[^:]*:(\d+):(\d+)(.*)$").unwrap());
let nice_error_option: Option<_> = try {
let captures = PATTERN.captures(&err.message())?;
let line = captures.get(1).unwrap().as_str().parse::<usize>().ok()?;
let msg = captures.get(3).unwrap().as_str();
let db = error_handling_ctx::FILE_DATABASE.read().ok()?;
let line_range = db.line_range(file_id, line - 1).ok()?;
let span = Span(line_range.start, line_range.end - 1, file_id);
DiagError(gen_diagnostic!(msg, span))
};
match nice_error_option {
Some(error) => Err(anyhow!(error)),
None => Err(anyhow!("CSS error: {}", err.message())),
}
} else {
Ok(())
}
}
}

Expand Down
10 changes: 5 additions & 5 deletions crates/eww/src/config/eww_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@ use eww_shared_util::VarName;
use std::collections::HashMap;
use yuck::{
config::{
file_provider::YuckFiles, script_var_definition::ScriptVarDefinition, validate::ValidationError,
widget_definition::WidgetDefinition, window_definition::WindowDefinition, Config,
script_var_definition::ScriptVarDefinition, validate::ValidationError, widget_definition::WidgetDefinition,
window_definition::WindowDefinition, Config,
},
error::DiagError,
format_diagnostic::ToDiagnostic,
};

use simplexpr::dynval::DynVal;

use crate::{config::inbuilt, error_handling_ctx, paths::EwwPaths, widgets::widget_definitions};
use crate::{config::inbuilt, error_handling_ctx, file_database::FileDatabase, paths::EwwPaths, widgets::widget_definitions};

use super::script_var;

/// Load an [`EwwConfig`] from the config dir of the given [`crate::EwwPaths`],
/// resetting and applying the global YuckFiles object in [`crate::error_handling_ctx`].
pub fn read_from_eww_paths(eww_paths: &EwwPaths) -> Result<EwwConfig> {
error_handling_ctx::clear_files();
EwwConfig::read_from_dir(&mut error_handling_ctx::YUCK_FILES.write().unwrap(), eww_paths)
EwwConfig::read_from_dir(&mut error_handling_ctx::FILE_DATABASE.write().unwrap(), eww_paths)
}

/// Eww configuration structure.
Expand Down Expand Up @@ -49,7 +49,7 @@ impl Default for EwwConfig {

impl EwwConfig {
/// Load an [`EwwConfig`] from the config dir of the given [`crate::EwwPaths`], reading the main config file.
pub fn read_from_dir(files: &mut YuckFiles, eww_paths: &EwwPaths) -> Result<Self> {
pub fn read_from_dir(files: &mut FileDatabase, eww_paths: &EwwPaths) -> Result<Self> {
let yuck_path = eww_paths.get_yuck_path();
if !yuck_path.exists() {
bail!("The configuration file `{}` does not exist", yuck_path.display());
Expand Down
11 changes: 8 additions & 3 deletions crates/eww/src/config/scss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ use std::path::Path;

use anyhow::{anyhow, Context};

use crate::util::replace_env_var_references;
use crate::{error_handling_ctx, util::replace_env_var_references};

/// read an scss file, replace all environment variable references within it and
/// then parse it into css.
pub fn parse_scss_from_file(path: &Path) -> anyhow::Result<String> {
/// Also adds the CSS to the [`crate::file_database::FileDatabase`]
pub fn parse_scss_from_file(path: &Path) -> anyhow::Result<(usize, String)> {
let config_dir = path.parent().context("Given SCSS file has no parent directory?!")?;
let scss_file_content =
std::fs::read_to_string(path).with_context(|| format!("Given SCSS-file doesn't exist! {}", path.display()))?;
let file_content = replace_env_var_references(scss_file_content);
let grass_config = grass::Options::default().load_path(config_dir);
grass::from_string(file_content, &grass_config).map_err(|err| anyhow!("Encountered SCSS parsing error: {}", err))
let css = grass::from_string(file_content, &grass_config).map_err(|err| anyhow!("SCSS parsing error: {}", err))?;

let mut file_db = error_handling_ctx::FILE_DATABASE.write().unwrap();
let file_id = file_db.insert_string(path.display().to_string(), css.clone())?;
Ok((file_id, css))
}
26 changes: 9 additions & 17 deletions crates/eww/src/error_handling_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,23 @@ use codespan_reporting::{
use eww_shared_util::Span;
use once_cell::sync::Lazy;
use simplexpr::{dynval::ConversionError, eval::EvalError};
use yuck::{
config::{file_provider::YuckFiles, validate::ValidationError},
error::DiagError,
format_diagnostic::ToDiagnostic,
};
use yuck::{config::validate::ValidationError, error::DiagError, format_diagnostic::ToDiagnostic};

use crate::file_database::FileDatabase;

pub static YUCK_FILES: Lazy<Arc<RwLock<YuckFiles>>> = Lazy::new(|| Arc::new(RwLock::new(YuckFiles::new())));
pub static FILE_DATABASE: Lazy<Arc<RwLock<FileDatabase>>> = Lazy::new(|| Arc::new(RwLock::new(FileDatabase::new())));

pub fn clear_files() {
*YUCK_FILES.write().unwrap() = YuckFiles::new();
*FILE_DATABASE.write().unwrap() = FileDatabase::new();
}

pub fn print_error(err: anyhow::Error) {
match anyhow_err_to_diagnostic(&err) {
Some(diag) => match stringify_diagnostic(diag) {
Ok(diag) => {
eprintln!("{}", diag);
}
Err(_) => {
log::error!("{:?}", err);
}
Ok(diag) => eprintln!("{}", diag),
Err(_) => log::error!("{:?}", err),
},
None => {
log::error!("{:?}", err);
}
None => log::error!("{:?}", err),
}
}

Expand Down Expand Up @@ -69,7 +61,7 @@ pub fn stringify_diagnostic(mut diagnostic: codespan_reporting::diagnostic::Diag
config.chars.note_bullet = '→';
let mut buf = Vec::new();
let mut writer = term::termcolor::Ansi::new(&mut buf);
let files = YUCK_FILES.read().unwrap();
let files = FILE_DATABASE.read().unwrap();
term::emit(&mut writer, &config, &*files, &diagnostic)?;
Ok(String::from_utf8(buf)?)
}
131 changes: 131 additions & 0 deletions crates/eww/src/file_database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use std::collections::HashMap;

use codespan_reporting::files::Files;
use eww_shared_util::Span;
use yuck::{
config::file_provider::{FilesError, YuckFileProvider},
error::DiagError,
parser::ast::Ast,
};

#[derive(Debug, Clone, Default)]
pub struct FileDatabase {
files: HashMap<usize, CodeFile>,
latest_id: usize,
}

impl FileDatabase {
pub fn new() -> Self {
Self::default()
}

fn get_file(&self, id: usize) -> Result<&CodeFile, codespan_reporting::files::Error> {
self.files.get(&id).ok_or(codespan_reporting::files::Error::FileMissing)
}

fn insert_code_file(&mut self, file: CodeFile) -> usize {
let file_id = self.latest_id;
self.files.insert(file_id, file);
self.latest_id += 1;
file_id
}

pub fn insert_string(&mut self, name: String, content: String) -> Result<usize, DiagError> {
let line_starts = codespan_reporting::files::line_starts(&content).collect();
let code_file = CodeFile { name, line_starts, source_len_bytes: content.len(), source: CodeSource::Literal(content) };
let file_id = self.insert_code_file(code_file);
Ok(file_id)
}
}

impl YuckFileProvider for FileDatabase {
fn load_yuck_file(&mut self, path: std::path::PathBuf) -> Result<(Span, Vec<Ast>), FilesError> {
let file_content = std::fs::read_to_string(&path)?;
let line_starts = codespan_reporting::files::line_starts(&file_content).collect();
let code_file = CodeFile {
name: path.display().to_string(),
line_starts,
source_len_bytes: file_content.len(),
source: CodeSource::File(path),
};
let file_id = self.insert_code_file(code_file);
Ok(yuck::parser::parse_toplevel(file_id, file_content)?)
}

fn load_yuck_str(&mut self, name: String, content: String) -> Result<(Span, Vec<Ast>), DiagError> {
let file_id = self.insert_string(name, content.clone())?;
yuck::parser::parse_toplevel(file_id, content)
}

fn unload(&mut self, id: usize) {
self.files.remove(&id);
}
}

impl<'a> Files<'a> for FileDatabase {
type FileId = usize;
type Name = &'a str;
type Source = String;

fn name(&'a self, id: Self::FileId) -> Result<Self::Name, codespan_reporting::files::Error> {
Ok(&self.get_file(id)?.name)
}

fn source(&'a self, id: Self::FileId) -> Result<Self::Source, codespan_reporting::files::Error> {
self.get_file(id)?.source.read_content().map_err(codespan_reporting::files::Error::Io)
}

fn line_index(&self, id: Self::FileId, byte_index: usize) -> Result<usize, codespan_reporting::files::Error> {
Ok(self.get_file(id)?.line_starts.binary_search(&byte_index).unwrap_or_else(|next_line| next_line - 1))
}

fn line_range(
&self,
id: Self::FileId,
line_index: usize,
) -> Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
let file = self.get_file(id)?;
let line_start = file.line_start(line_index)?;
let next_line_start = file.line_start(line_index + 1)?;
Ok(line_start..next_line_start)
}
}

#[derive(Clone, Debug)]
struct CodeFile {
name: String,
line_starts: Vec<usize>,
source: CodeSource,
source_len_bytes: usize,
}

impl CodeFile {
/// Return the starting byte index of the line with the specified line index.
/// Convenience method that already generates errors if necessary.
fn line_start(&self, line_index: usize) -> Result<usize, codespan_reporting::files::Error> {
use std::cmp::Ordering;

match line_index.cmp(&self.line_starts.len()) {
Ordering::Less => Ok(self.line_starts.get(line_index).cloned().expect("failed despite previous check")),
Ordering::Equal => Ok(self.source_len_bytes),
Ordering::Greater => {
Err(codespan_reporting::files::Error::LineTooLarge { given: line_index, max: self.line_starts.len() - 1 })
}
}
}
}

#[derive(Clone, Debug)]
enum CodeSource {
File(std::path::PathBuf),
Literal(String),
}

impl CodeSource {
fn read_content(&self) -> std::io::Result<String> {
match self {
CodeSource::File(path) => Ok(std::fs::read_to_string(path)?),
CodeSource::Literal(x) => Ok(x.to_string()),
}
}
}
1 change: 1 addition & 0 deletions crates/eww/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod config;
mod daemon_response;
mod display_backend;
mod error_handling_ctx;
mod file_database;
mod geometry;
mod ipc_server;
mod opts;
Expand Down
6 changes: 4 additions & 2 deletions crates/eww/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ pub fn initialize_server(paths: EwwPaths, action: Option<DaemonCommand>, should_
gtk::StyleContext::add_provider_for_screen(&screen, &app.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
}

if let Ok(eww_css) = config::scss::parse_scss_from_file(&app.paths.get_eww_scss_path()) {
app.load_css(&eww_css)?;
if let Ok((file_id, css)) = config::scss::parse_scss_from_file(&app.paths.get_eww_scss_path()) {
if let Err(e) = app.load_css(file_id, &css) {
error_handling_ctx::print_error(e);
}
}

// initialize all the handlers and tasks running asyncronously
Expand Down
Loading

0 comments on commit 7623e7e

Please sign in to comment.