| 5002 |
5002 |
|
}
|
| 5003 |
5003 |
|
}
|
| 5004 |
5004 |
|
|
|
5005 |
+ |
/// What a run of source code is, once something has classified it.
|
|
5006 |
+ |
///
|
|
5007 |
+ |
/// The description carries the classification and never the source, which is
|
|
5008 |
+ |
/// the whole of decision `19d7602d` (2026-09-02, option d). An app that browses
|
|
5009 |
+ |
/// source already has a lexer; a renderer does not and should not grow one, and
|
|
5010 |
+ |
/// three renderers each growing their own would disagree about the same file.
|
|
5011 |
+ |
///
|
|
5012 |
+ |
/// # Why the app classifies and the renderer colours
|
|
5013 |
+ |
///
|
|
5014 |
+ |
/// Measured on MNW's source browser, which is the only consumer in the tree.
|
|
5015 |
+ |
/// It highlights server-side with syntect and had already reduced syntect's
|
|
5016 |
+ |
/// scope space to seven colours held fixed across all 31 themes, because a
|
|
5017 |
+ |
/// reader recognises a highlighting palette and re-tinting it per theme costs
|
|
5018 |
+ |
/// that recognition to gain nothing. So the classification existed on the app
|
|
5019 |
+ |
/// side already: the only question was whether to throw it away at the seam and
|
|
5020 |
+ |
/// have each renderer redo it. This is the answer.
|
|
5021 |
+ |
///
|
|
5022 |
+ |
/// The precedent is `docengine`'s `Emphasis`, which crosses the same seam the
|
|
5023 |
+ |
/// same way: `quasi-tui` maps its four flags onto terminal modifiers, a webview
|
|
5024 |
+ |
/// maps them onto elements, and neither parses markdown to do it.
|
|
5025 |
+ |
///
|
|
5026 |
+ |
/// # The eight, and why these eight
|
|
5027 |
+ |
///
|
|
5028 |
+ |
/// The seven MNW's palette fixes, plus [`Plain`](Self::Plain) for a run nothing
|
|
5029 |
+ |
/// claimed. `Plain` is not an absence: a lexer that ran and found ordinary code
|
|
5030 |
+ |
/// is saying something a renderer wants, and an `Option<Syntax>` would have made
|
|
5031 |
+ |
/// "unclassified" and "not classified yet" one value.
|
|
5032 |
+ |
///
|
|
5033 |
+ |
/// `#[non_exhaustive]` from the first commit, deliberately. A ninth class is the
|
|
5034 |
+ |
/// obvious next request and it must not be a breaking release across three
|
|
5035 |
+ |
/// renderers and five apps.
|
|
5036 |
+ |
///
|
|
5037 |
+ |
/// # What it is not
|
|
5038 |
+ |
///
|
|
5039 |
+ |
/// A token type in a grammar. These are display classes, coarse on purpose:
|
|
5040 |
+ |
/// the distinctions a reader uses at a glance, not the ones a parser makes.
|
|
5041 |
+ |
/// A renderer wanting more has `language` on the node beside this and may do
|
|
5042 |
+ |
/// whatever it likes with it.
|
|
5043 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
5044 |
+ |
#[non_exhaustive]
|
|
5045 |
+ |
pub enum Syntax {
|
|
5046 |
+ |
/// Ordinary code nothing else claimed.
|
|
5047 |
+ |
///
|
|
5048 |
+ |
/// The default, and a real answer rather than a missing one. See the type's
|
|
5049 |
+ |
/// docs for why this is not an `Option`.
|
|
5050 |
+ |
#[default]
|
|
5051 |
+ |
Plain,
|
|
5052 |
+ |
/// A language keyword, and the storage and modifier words with it.
|
|
5053 |
+ |
Keyword,
|
|
5054 |
+ |
/// A string or character literal.
|
|
5055 |
+ |
String,
|
|
5056 |
+ |
/// A comment.
|
|
5057 |
+ |
Comment,
|
|
5058 |
+ |
/// A literal that is not a string: a number, a boolean, a constant name.
|
|
5059 |
+ |
Constant,
|
|
5060 |
+ |
/// A name being defined: a function, a type, a module.
|
|
5061 |
+ |
Entity,
|
|
5062 |
+ |
/// A name being used: a variable, a parameter, a field.
|
|
5063 |
+ |
Variable,
|
|
5064 |
+ |
/// Something the language or its library provides rather than this file.
|
|
5065 |
+ |
Support,
|
|
5066 |
+ |
}
|
|
5067 |
+ |
|
|
5068 |
+ |
impl Syntax {
|
|
5069 |
+ |
/// The stable lowercase name, for a renderer keying its own palette off it.
|
|
5070 |
+ |
///
|
|
5071 |
+ |
/// Named here rather than agreed between each renderer and each host, for
|
|
5072 |
+ |
/// the reason every other spelling in this crate is: that is how one
|
|
5073 |
+ |
/// renderer ends up calling it `str` and the next `string`, and a theme
|
|
5074 |
+ |
/// written against one stops working under the other.
|
|
5075 |
+ |
///
|
|
5076 |
+ |
/// Not an [`Intent`] token, and the difference is worth stating. An intent
|
|
5077 |
+ |
/// resolves against a `makeover` theme token, and there are none for syntax
|
|
5078 |
+ |
/// colours: a highlighting palette is deliberately outside the theme, held
|
|
5079 |
+ |
/// fixed while everything around it changes. So this is a name a renderer
|
|
5080 |
+ |
/// maps however it can, and a renderer with no colours to spend maps every
|
|
5081 |
+ |
/// one of them onto the same face and is not wrong.
|
|
5082 |
+ |
#[must_use]
|
|
5083 |
+ |
pub const fn name(self) -> &'static str {
|
|
5084 |
+ |
match self {
|
|
5085 |
+ |
Self::Plain => "plain",
|
|
5086 |
+ |
Self::Keyword => "keyword",
|
|
5087 |
+ |
Self::String => "string",
|
|
5088 |
+ |
Self::Comment => "comment",
|
|
5089 |
+ |
Self::Constant => "constant",
|
|
5090 |
+ |
Self::Entity => "entity",
|
|
5091 |
+ |
Self::Variable => "variable",
|
|
5092 |
+ |
Self::Support => "support",
|
|
5093 |
+ |
// Exhaustive rather than wildcarded, which is `State`'s rule one
|
|
5094 |
+ |
// type up: within this crate `#[non_exhaustive]` does not apply, so
|
|
5095 |
+ |
// a class added here has to be given a spelling rather than
|
|
5096 |
+ |
// inheriting "plain" in silence. Consumers outside the crate take
|
|
5097 |
+ |
// the wildcard, and for them "plain" is the right degradation.
|
|
5098 |
+ |
}
|
|
5099 |
+ |
}
|
|
5100 |
+ |
}
|
|
5101 |
+ |
|
|
5102 |
+ |
/// What one line of a diff is: added, removed, or neither.
|
|
5103 |
+ |
///
|
|
5104 |
+ |
/// The other half of decision `19d7602d`. A diff is a table of lines and the
|
|
5105 |
+ |
/// only thing the vocabulary was missing was a way for a line to say which side
|
|
5106 |
+ |
/// of the change it is on, so this rides on [`Cells`] rather than arriving as a
|
|
5107 |
+ |
/// `Node::Diff` carrying git's data model.
|
|
5108 |
+ |
///
|
|
5109 |
+ |
/// # Why three and not two
|
|
5110 |
+ |
///
|
|
5111 |
+ |
/// [`Context`](Self::Context) is a line that did not change, and it is most of
|
|
5112 |
+ |
/// a diff. Said as `Option<Change>` with `None` for context, a renderer could
|
|
5113 |
+ |
/// not tell an unchanged line from a line nobody marked, which is the same
|
|
5114 |
+ |
/// argument [`Syntax::Plain`] makes one type up.
|
|
5115 |
+ |
///
|
|
5116 |
+ |
/// `#[non_exhaustive]` for [`Syntax`]'s reason. A fourth kind -- a moved line, a
|
|
5117 |
+ |
/// conflict side -- is a plausible request and must not be a breaking release.
|
|
5118 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
5119 |
+ |
#[non_exhaustive]
|
|
5120 |
+ |
pub enum Change {
|
|
5121 |
+ |
/// The line is in both sides and did not change.
|
|
5122 |
+ |
#[default]
|
|
5123 |
+ |
Context,
|
|
5124 |
+ |
/// The line is only in the new side.
|
|
5125 |
+ |
Added,
|
|
5126 |
+ |
/// The line is only in the old side.
|
|
5127 |
+ |
Removed,
|
|
5128 |
+ |
}
|
|
5129 |
+ |
|
|
5130 |
+ |
impl Intent for Change {
|
|
5131 |
+ |
fn token(self) -> &'static str {
|
|
5132 |
+ |
match self {
|
|
5133 |
+ |
// The status axis, reused rather than given a palette of its own.
|
|
5134 |
+ |
// A diff's green and red mean exactly what success and danger mean
|
|
5135 |
+ |
// everywhere else in the system, and an app that wants them held
|
|
5136 |
+ |
// fixed across themes says so in its own sheet, which is what MNW's
|
|
5137 |
+ |
// `--diff-add` / `--diff-del` pair already does.
|
|
5138 |
+ |
Self::Added => "success",
|
|
5139 |
+ |
Self::Removed => "danger",
|
|
5140 |
+ |
// Context is the bulk of a diff and is ordinary content. Muting it
|
|
5141 |
+ |
// would be the axis deciding that unchanged lines are less worth
|
|
5142 |
+ |
// reading, which is a renderer's call about a particular view.
|
|
5143 |
+ |
//
|
|
5144 |
+ |
// Exhaustive for `Syntax::name`'s reason: a fourth kind must answer
|
|
5145 |
+ |
// this rather than inherit an answer.
|
|
5146 |
+ |
Self::Context => "content",
|
|
5147 |
+ |
}
|
|
5148 |
+ |
}
|
|
5149 |
+ |
}
|
|
5150 |
+ |
|
| 5005 |
5151 |
|
#[cfg(test)]
|
| 5006 |
5152 |
|
mod tests {
|
|
5153 |
+ |
use super::{Change, Intent as _, Syntax};
|
|
5154 |
+ |
|
|
5155 |
+ |
/// A run nothing claimed is a real answer rather than a missing one, which
|
|
5156 |
+ |
/// is why this is not an `Option<Syntax>`: a lexer that ran and found
|
|
5157 |
+ |
/// ordinary code has said something.
|
|
5158 |
+ |
#[test]
|
|
5159 |
+ |
fn an_unclassified_run_is_plain_and_plain_is_the_default() {
|
|
5160 |
+ |
assert_eq!(Syntax::default(), Syntax::Plain);
|
|
5161 |
+ |
assert_eq!(Syntax::Plain.name(), "plain");
|
|
5162 |
+ |
}
|
|
5163 |
+ |
|
|
5164 |
+ |
/// Every class has a stable lowercase name, and no two share one. The
|
|
5165 |
+ |
/// spelling is named here rather than agreed per renderer, which is what
|
|
5166 |
+ |
/// stops one calling it `str` and the next `string`.
|
|
5167 |
+ |
#[test]
|
|
5168 |
+ |
fn every_syntax_class_has_its_own_spelling() {
|
|
5169 |
+ |
let all = [
|
|
5170 |
+ |
Syntax::Plain,
|
|
5171 |
+ |
Syntax::Keyword,
|
|
5172 |
+ |
Syntax::String,
|
|
5173 |
+ |
Syntax::Comment,
|
|
5174 |
+ |
Syntax::Constant,
|
|
5175 |
+ |
Syntax::Entity,
|
|
5176 |
+ |
Syntax::Variable,
|
|
5177 |
+ |
Syntax::Support,
|
|
5178 |
+ |
];
|
|
5179 |
+ |
let mut seen = std::collections::BTreeSet::new();
|
|
5180 |
+ |
for class in all {
|
|
5181 |
+ |
assert!(!class.name().is_empty(), "{class:?} has no spelling");
|
|
5182 |
+ |
assert!(seen.insert(class.name()), "{class:?} shares a spelling");
|
|
5183 |
+ |
}
|
|
5184 |
+ |
assert_eq!(seen.len(), all.len());
|
|
5185 |
+ |
}
|
|
5186 |
+ |
|
|
5187 |
+ |
/// Context is a state and not an absence, so an unchanged line is
|
|
5188 |
+ |
/// distinguishable from a line nobody marked. `Syntax::Plain`'s argument.
|
|
5189 |
+ |
#[test]
|
|
5190 |
+ |
fn an_unmarked_diff_line_is_context() {
|
|
5191 |
+ |
assert_eq!(Change::default(), Change::Context);
|
|
5192 |
+ |
assert_eq!(Change::Context.token(), "content");
|
|
5193 |
+ |
}
|
|
5194 |
+ |
|
|
5195 |
+ |
/// The two sides reuse the status axis rather than minting a palette. An
|
|
5196 |
+ |
/// app holding them fixed across themes says so in its own sheet.
|
|
5197 |
+ |
#[test]
|
|
5198 |
+ |
fn the_two_sides_of_a_change_take_the_status_intents() {
|
|
5199 |
+ |
assert_eq!(Change::Added.token(), "success");
|
|
5200 |
+ |
assert_eq!(Change::Removed.token(), "danger");
|
|
5201 |
+ |
}
|
|
5202 |
+ |
|
| 5007 |
5203 |
|
/// The concept is named here and its magnitude is not, which is
|
| 5008 |
5204 |
|
/// `Awaiting`'s split and is why a renderer can disagree with another
|
| 5009 |
5205 |
|
/// about what a level is worth without either of them being wrong.
|