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
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
16 changes: 16 additions & 0 deletions crates/server/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@ impl LanguageServer for Backend {
// automatically). Shape:
// `{ "format": {"enable": bool}, "schemaPath": "schema.json",
// "analysis": {"modelMemberStrictness": "compatible",
// "allowPercentagesWithoutSign": false,
// "mapOrderingDiagnostics": true, "debounceMs": 250},
// "baseIniRoots": ["dir-or-big", ...],
// "clientBaseIniHint": bool }`.
Expand Down Expand Up @@ -701,6 +702,14 @@ impl LanguageServer for Backend {
index.set_model_member_strictness(model_member_strictness);
}

let allow_bare_percentages = params
.initialization_options
.as_ref()
.and_then(|v| v.get("analysis"))
.and_then(|v| v.get("allowPercentagesWithoutSign"))
.and_then(|v| v.as_bool())
.unwrap_or(false);

if let Some(path) = params
.initialization_options
.as_ref()
Expand All @@ -716,6 +725,13 @@ impl LanguageServer for Backend {
*current = error;
}
}
if allow_bare_percentages {
if let Ok(mut current) = self.analyzer.write() {
Arc::get_mut(&mut current)
.expect("analyzer shared before initialization completed")
.set_allow_bare_percentages(true);
}
}

let base_roots = params
.initialization_options
Expand Down
3 changes: 3 additions & 0 deletions docs/language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ symbols.
"schemaPath": "C:/Mods/MyMod/schema.json",
"analysis": {
"modelMemberStrictness": "compatible",
"allowPercentagesWithoutSign": false,
"mapOrderingDiagnostics": true,
"debounceMs": 250
},
Expand All @@ -105,6 +106,8 @@ symbols.
- `analysis.modelMemberStrictness` is `off`, `compatible` (member exists in any
applicable model), or `strict` (member exists in every applicable model). It
defaults to `compatible`.
- `analysis.allowPercentagesWithoutSign` accepts engine-compatible bare numbers
in percentage fields. It defaults to `false`, requiring the trailing `%`.
- `analysis.mapOrderingDiagnostics` controls source-backed forward-order
warnings in `map.ini` and `solo.ini`. It defaults to `true`.
- `analysis.debounceMs` waits this many milliseconds after the latest edit
Expand Down
1 change: 1 addition & 0 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ textures are offered using the engine-compatible `stem.tga` spelling.
| `zerosyntax.baseIniRoots` | `[]` | Base game/mod directories and `.big` archives used for INI and game-asset checks. |
| `zerosyntax.schema.path` | empty | Custom schema JSON; invalid files fall back to the built-in schema. |
| `zerosyntax.analysis.modelMemberStrictness` | `compatible` | Disables member warnings, accepts any applicable model, or requires every model. |
| `zerosyntax.analysis.allowPercentagesWithoutSign` | `false` | Allows engine-compatible percentage values without a trailing `%`. |
| `zerosyntax.analysis.mapOrderingDiagnostics` | `true` | Warns about source-proven forward-order problems in `map.ini` and `solo.ini`. |
| `zerosyntax.format.enable` | `false` | Enables indentation formatting. Changing it restarts the server. |
| `zerosyntax.server.path` | empty | Uses a custom `zerosyntax-lsp` binary instead of the bundled one. |
Expand Down
5 changes: 5 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
"default": "compatible",
"markdownDescription": "Model-member diagnostics: off disables warnings, compatible accepts a bone/subobject present in any applicable model, and strict requires it in every applicable model. Changing this restarts the language server."
},
"zerosyntax.analysis.allowPercentagesWithoutSign": {
"type": "boolean",
"default": false,
"markdownDescription": "Allow engine-compatible percentage values without a trailing `%` sign. Changing this restarts the language server."
},
"zerosyntax.analysis.mapOrderingDiagnostics": {
"type": "boolean",
"default": true,
Expand Down
31 changes: 31 additions & 0 deletions editors/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {

let client: LanguageClient | undefined;
let baseIniRootsHintShown = false;
const allowBarePercentagesSetting = "analysis.allowPercentagesWithoutSign";

export function activate(context: vscode.ExtensionContext) {
const serverPath = resolveServerPath(context);
Expand Down Expand Up @@ -42,6 +43,7 @@ export function activate(context: vscode.ExtensionContext) {
schemaPath: setting<string>("schema.path", ""),
analysis: {
modelMemberStrictness: setting<string>("analysis.modelMemberStrictness", "compatible"),
allowPercentagesWithoutSign: setting<boolean>(allowBarePercentagesSetting, false),
mapOrderingDiagnostics: setting<boolean>("analysis.mapOrderingDiagnostics", true),
debounceMs: setting<number>("analysis.debounceMs", 250),
},
Expand Down Expand Up @@ -83,6 +85,35 @@ export function activate(context: vscode.ExtensionContext) {
.update("schema.path", selected[0].fsPath, vscode.ConfigurationTarget.Workspace);
}
}),
vscode.commands.registerCommand("zerosyntax.allowBarePercentages", async () => {
await vscode.workspace
.getConfiguration("zerosyntax")
.update(allowBarePercentagesSetting, true, vscode.ConfigurationTarget.Global);
}),
vscode.languages.registerCodeActionsProvider(
{ scheme: "file", language: "generals-ini" },
{
provideCodeActions(_document, _range, actionContext) {
const diagnostics = actionContext.diagnostics.filter(
(diagnostic) => diagnostic.code === "bad-percent"
);
if (diagnostics.length === 0) {
return [];
}
const action = new vscode.CodeAction(
"Allow percentages without `%`",
vscode.CodeActionKind.QuickFix
);
action.diagnostics = diagnostics;
action.command = {
command: "zerosyntax.allowBarePercentages",
title: action.title,
};
return [action];
},
},
{ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }
),
vscode.workspace.onDidOpenTextDocument((document) => {
void maybeShowBaseIniRootsHint(document);
})
Expand Down
18 changes: 17 additions & 1 deletion editors/vscode/src/test/suite/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ suite("ZeroSyntax VS Code extension", () => {
const uri = vscode.Uri.file(path.join(dir, "Smoke.ini"));
await vscode.workspace.fs.writeFile(
uri,
Buffer.from("Weapon SmokeGun\n ScaleWeaponSpeed = Maybe\n \nEnd\n")
Buffer.from(
"Weapon SmokeGun\n ScaleWeaponSpeed = Maybe\n \nEnd\n" +
"Armor SmokeArmor\n Armor = ARMOR_PIERCING 2\nEnd\n"
)
);

const document = await vscode.workspace.openTextDocument(uri);
Expand Down Expand Up @@ -46,6 +49,19 @@ suite("ZeroSyntax VS Code extension", () => {
labels.includes("PrimaryDamage"),
`expected PrimaryDamage completion, got ${labels.slice(0, 10).join(", ")}`
);

const percentDiagnostic = diagnostics.find((diag) => diag.code === "bad-percent");
assert.ok(percentDiagnostic, "expected a bad-percent diagnostic");
const actions = await vscode.commands.executeCommand<(vscode.CodeAction | vscode.Command)[]>(
"vscode.executeCodeActionProvider",
uri,
percentDiagnostic.range,
vscode.CodeActionKind.QuickFix.value
);
assert.ok(
actions.some((action) => action.title === "Allow percentages without `%`"),
"expected the bare-percentage settings quick fix"
);
});
});

Expand Down