Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,14 @@ schema.json ──embedded──▶ schema ──▶ analysis ──▶ server
`WorkspaceIndex`. `convert.rs` does byte-offset ↔ LSP position mapping in
the position encoding negotiated at `initialize` (UTF-8 when the client
offers it, else the UTF-16 baseline). Advertises `semanticTokens` `full`
**and** `range`. Formatting is **opt-in**: the capability is advertised
only when `initializationOptions` carries `{"format": {"enable": true}}`
(the VS Code setting `zerosyntax.format.enable`, default off — real game
files are wildly hand-indented, so format-on-save must never fire
unasked). Phase-3 numbers (`docs/phase3-incremental.md`): keystroke on the
**and** `range`. Runtime settings arrive through initialization options and
`workspace/didChangeConfiguration`; analysis switches refresh open docs,
while schema/base-root changes replace the complete index and reparse only
when the schema changes. Formatting is **opt-in** and dynamically registered
when supported (the VS Code setting `zerosyntax.format.enable`, default off —
real game files are wildly hand-indented, so format-on-save must never fire
unasked). Only the executable-path setting requires a client restart.
Phase-3 numbers (`docs/phase3-incremental.md`): keystroke on the
61k-line ParticleSystem.ini ≈ 147 µs vs 44 ms full reparse.

### Two concepts worth understanding before editing
Expand Down
21 changes: 7 additions & 14 deletions crates/analysis/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use zerosyntax_schema::{RefKind, ValueType};
use zerosyntax_syntax::ast::{Field, Module};
use zerosyntax_syntax::{Parse, SyntaxErrorKind, SyntaxKind, SyntaxNode, SyntaxToken};

use crate::diagnostics::{pragma_rest, pragma_words, Diagnostic, Severity};
use crate::diagnostics::{pragma_rest, pragma_words, Diagnostic};
use crate::model::scope_schema;
use crate::{nav, Analyzer, Span, WorkspaceIndex};

Expand Down Expand Up @@ -203,12 +203,8 @@ fn diagnostic_fixes(
}
_ => {}
}
// Suppress pragma: warnings and hints only; never errors, never the
// misspelled-suppression hint (suppressing it is self-defeating).
if d.severity != Severity::Error
&& d.code != "unknown-suppression"
&& suppress_seen.insert(d.code)
{
// Suppressing the misspelled-suppression hint is self-defeating.
if d.code != "unknown-suppression" && suppress_seen.insert(d.code) {
suppress_fix(parse, text, d.code, out);
}
}
Expand Down Expand Up @@ -429,7 +425,7 @@ fn stub_keyword<'a>(analyzer: &'a Analyzer, kind: RefKind) -> Option<&'a str> {
}

/// Offer to add `; zerosyntax-disable: <code>` at the top of the file (or
/// append to an existing pragma line) for warning/hint diagnostics.
/// append to an existing pragma line) for a diagnostic.
fn suppress_fix(parse: &Parse, text: &str, code: &'static str, out: &mut Vec<Fix>) {
let root = parse.syntax();
let mut first_pragma: Option<(u32, bool)> = None; // (insert offset, has_any_codes)
Expand Down Expand Up @@ -923,17 +919,14 @@ mod tests {
}

#[test]
fn suppress_fix_severity_and_dedupe_rules() {
// bad-bool is Error severity → no Suppress action.
fn suppress_fix_errors_and_dedupes() {
// Error diagnostics get the same Suppress action as warnings.
let src_err = "Weapon W\n ScaleWeaponSpeed = Maybe\nEnd\n";
let fx_err = all_fixes(src_err);
let has_suppress_for_error = fx_err
.iter()
.any(|f| f.title.contains("Suppress") && f.title.contains("bad-bool"));
assert!(
!has_suppress_for_error,
"errors must not get suppress: {fx_err:?}"
);
assert!(has_suppress_for_error, "error missing suppress: {fx_err:?}");

// Two unresolved references with the same code → only one Suppress action.
let src_dup = "Weapon W\n FireFX = NoSuchFX\n FireFX = NoSuchFX2\nEnd\n";
Expand Down
26 changes: 21 additions & 5 deletions crates/analysis/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1536,12 +1536,10 @@ impl<'a> Ctx<'a> {
}
}
}
// Stricter than the engine (which reads a bare real): require the
// `%` sign, because `Armor = X 2` almost never means 2 percent.
ValueType::Percent => {
let ok = tok
.text()
.strip_suffix('%')
let value = tok.text().strip_suffix('%');
let ok = value
.or_else(|| self.analyzer.allow_bare_percentages().then_some(tok.text()))
.is_some_and(|n| n.parse::<f64>().is_ok());
if !ok {
self.error(
Expand Down Expand Up @@ -1980,6 +1978,24 @@ mod tests {
assert!(diags(src).is_empty(), "{:?}", diags(src));
}

#[test]
fn bare_percentages_are_opt_in() {
let src = "Armor A\n Armor = ARMOR_PIERCING 2.5\nEnd\n";
assert!(codes(src).contains(&"bad-percent"));

let mut analyzer = Analyzer::embedded();
analyzer.set_allow_bare_percentages(true);
let parse = analyzer.parse(src);
assert!(!diagnose(&analyzer, &parse, None, None)
.iter()
.any(|d| d.code == "bad-percent"));

let malformed = analyzer.parse("Armor A\n Armor = ARMOR_PIERCING nope\nEnd\n");
assert!(diagnose(&analyzer, &malformed, None, None)
.iter()
.any(|d| d.code == "bad-percent"));
}

#[test]
fn unknown_block_is_error() {
assert!(codes("Wepon AK47\nEnd\n").contains(&"unknown-block"));
Expand Down
11 changes: 11 additions & 0 deletions crates/analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl From<rowan::TextRange> for Span {
/// it. Cheap to share; build once and reuse across documents.
pub struct Analyzer {
schema: Schema,
allow_bare_percentages: bool,
block_by_name: HashMap<String, usize>,
module_by_name: HashMap<String, usize>,
value_set_by_id: HashMap<String, usize>,
Expand Down Expand Up @@ -88,6 +89,7 @@ impl Analyzer {
let openers = SchemaOpeners::from_schema(&schema);
Analyzer {
schema,
allow_bare_percentages: false,
block_by_name,
module_by_name,
value_set_by_id,
Expand All @@ -105,6 +107,15 @@ impl Analyzer {
&self.schema
}

/// Allow engine-compatible percentage values without a trailing `%`.
pub fn set_allow_bare_percentages(&mut self, allow: bool) {
self.allow_bare_percentages = allow;
}

pub fn allow_bare_percentages(&self) -> bool {
self.allow_bare_percentages
}

/// Parse `src` using the schema-derived opener oracle.
pub fn parse(&self, src: &str) -> Parse {
parse(src, &self.openers)
Expand Down
2 changes: 1 addition & 1 deletion crates/analysis/tests/spec/QuickfixSuppress.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
; Quickfix test: suppress-in-file pragma.

; bad-bool fires an Error on "Maybe" → no Suppress action.
; bad-bool fires an Error on "Maybe" → Suppress action offered.
Weapon QFSuppressWeapon
ScaleWeaponSpeed = Maybe
FireFX = UnknownFXRef
Expand Down
4 changes: 2 additions & 2 deletions crates/analysis/tests/spec/QuickfixSuppress.spec.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# "Maybe" fires bad-bool (Error) → no Suppress quickfix
# "Maybe" fires bad-bool (Error) → Suppress quickfix offered
[[diag]]
severity = "error"
code = "bad-bool"
on = "Maybe"

[[action]]
on = "Maybe"
not_offers = ["Suppress"]
offers = ["Suppress"]

# UnknownFXRef fires unresolved-reference (Warning) → Suppress quickfix offered
[[diag]]
Expand Down
Loading