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
9 changes: 5 additions & 4 deletions Cargo.lock

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

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/schema", "crates/syntax", "crates/analysis", "crates/server"]

[workspace.package]
version = "1.1.0"
version = "1.2.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/ViTeXFTW/ZeroSyntaxV2"
Expand Down Expand Up @@ -34,6 +34,12 @@ dashmap = "6"

# server (workspace indexing)
walkdir = "2"
clap = { version = "4", default-features = false, features = [
"std",
"help",
"usage",
"error-context",
] }

# benches
criterion = "0.8"
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,32 @@ extract it, and configure your editor to run the binary over stdio.
See the [language server guide](docs/language-server.md) for initialization
options and editor integration details.

## Check files from the command line

The same binary can run diagnostics without an editor:

```sh
zerosyntax-lsp check Data/INI
zerosyntax-lsp check map.ini --base-root "C:/Games/Zero Hour"
zerosyntax-lsp check --json --stdin-filename map.ini - < generated.ini
```

This is intended for CI, pre-commit checks, and LLM edit/check loops. Errors
produce exit code 1 by default; add `--fail-on warning` for a stricter gate.
See the [standalone guide](docs/language-server.md#command-line-diagnostics) for
the complete output and exit-code contract.

## Configure map and model checks

For complete `map.ini` and `solo.ini` diagnostics, set
`zerosyntax.baseIniRoots` in VS Code to the base game or mod directories and/or
`.big` archives that load before the map. The same setting also enables W3D
model and bone completion and validation.

`zerosyntax.analysis.modelMemberStrictness` controls bone/subobject warnings:
`off`, `compatible` (the default; present in any applicable model), or `strict`
(present in every applicable model).

```json
{
"zerosyntax.baseIniRoots": [
Expand Down
11 changes: 9 additions & 2 deletions crates/analysis/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,15 @@ fn suggest_in_field(analyzer: &Analyzer, node: &SyntaxNode, range: Span, out: &m
check(tok, value_set, true);
}
}
ValueType::TokenList { tokens: specs } => {
for (spec, tok) in specs.iter().zip(tokens.iter()) {
ValueType::TokenList { .. } | ValueType::OneOf { .. } | ValueType::Prefixed { .. } => {
let input = tokens
.iter()
.map(|token| token.text().trim_matches('"'))
.collect::<Vec<_>>();
for (index, tok) in tokens.iter().enumerate() {
let Some(spec) = schema_field.value_type.token_type_at_input(&input, index) else {
continue;
};
match spec {
ValueType::Enum { value_set } => check(tok, value_set, false),
ValueType::BitFlags { value_set } => check(tok, value_set, true),
Expand Down
83 changes: 73 additions & 10 deletions crates/analysis/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use zerosyntax_syntax::ast::{Block, Field, Module};
use zerosyntax_syntax::{Parse, SyntaxKind, SyntaxNode};

use crate::model::{
is_model_asset_type, is_model_member_type, model_member_ini_name, models_in_scope, scope_schema,
is_model_asset_type, is_model_member_type, model_member_ini_name, models_for_source,
scope_schema,
};
use crate::{Analyzer, WorkspaceIndex};

Expand Down Expand Up @@ -118,17 +119,48 @@ fn classify_position(analyzer: &Analyzer, root: &SyntaxNode, offset: u32) -> Pos
.map(|k| k.text().to_string())
.unwrap_or_default();
let value_tokens = field.value_tokens();
let value_index = value_tokens
let raw_value_index = value_tokens
.iter()
.filter(|t| u32::from(t.text_range().end()) < offset)
.count();
let input = value_tokens
.iter()
.map(|token| token.text().trim_matches('"'))
.collect::<Vec<_>>();
let value_index = scope_node
.as_ref()
.and_then(|scope_node| scope_schema(analyzer, scope_node).field(&key))
.and_then(|field| {
field
.value_type
.token_index_at_input(&input, raw_value_index)
})
.unwrap_or(raw_value_index);
let current_token = value_tokens
.iter()
.find(|t| {
.position(|t| {
let range = t.text_range();
u32::from(range.start()) <= offset && offset <= u32::from(range.end())
})
.map(|t| t.text().trim_matches('"').to_string());
.map(|index| {
let current = value_tokens[index].text().trim_matches('"');
index
.checked_sub(1)
.and_then(|index| value_tokens.get(index))
.map(|previous| previous.text().trim_matches('"'))
.filter(|previous| previous.ends_with(':'))
.map_or_else(
|| current.to_string(),
|previous| format!("{previous}{current}"),
)
})
.or_else(|| {
raw_value_index
.checked_sub(1)
.and_then(|index| input.get(index))
.filter(|previous| previous.ends_with(':'))
.map(|previous| (*previous).to_string())
});
let first_token = value_tokens
.first()
.map(|t| t.text().trim_matches('"').to_string());
Expand Down Expand Up @@ -325,9 +357,14 @@ fn field_value_completions(
let mut base = {
let scope = scope_schema(analyzer, scope_node);
if let Some(f) = scope.field(key) {
if let Some(asset_completions) =
model_asset_completions(analyzer, scope_node, &f.value_type, value_index, index)
{
if let Some(asset_completions) = model_asset_completions(
analyzer,
scope_node,
&f.value_type,
value_index,
index,
f.model_source.as_ref(),
) {
asset_completions
} else {
completions_for_type(
Expand Down Expand Up @@ -362,6 +399,7 @@ fn model_asset_completions(
ty: &ValueType,
value_index: usize,
index: Option<&WorkspaceIndex>,
source: Option<&zerosyntax_schema::ModelSource>,
) -> Option<Vec<Completion>> {
let index = index?;
if !index.has_model_assets() {
Expand All @@ -385,7 +423,7 @@ fn model_asset_completions(
return None;
}
let mut seen = std::collections::HashSet::new();
let out = models_in_scope(analyzer, scope_node)
let out = models_for_source(analyzer, scope_node, source, index)
.into_iter()
.flat_map(|model| {
index
Expand Down Expand Up @@ -904,6 +942,31 @@ End
assert!(out.contains(&"Good".to_string()), "{out:?}");
}

#[test]
fn ocl_member_completions_use_transport_models() {
let a = Analyzer::embedded();
let mut index = WorkspaceIndex::new();
index.set_file_models(
"a10.w3d",
vec![crate::index::ModelAsset {
name: "A10".into(),
members: vec!["WeaponA01".into()],
}],
);
index.set_file_object_models(
"objects.ini",
vec![("AmericaJetA10Thunderbolt".into(), vec!["A10".into()])],
);
let src = "ObjectCreationList Strike\n DeliverPayload\n Transport = AmericaJetA10Thunderbolt\n VisibleDropBoneBaseName = \n End\nEnd\n";
let offset =
src.find("VisibleDropBoneBaseName = ").unwrap() + "VisibleDropBoneBaseName = ".len();
let out = complete(&a, &a.parse(src), offset as u32, Some(&index), None)
.into_iter()
.map(|item| item.label)
.collect::<Vec<_>>();
assert!(out.contains(&"WeaponA".to_string()), "{out:?}");
}

#[test]
fn weapon_bone_completions_use_token_positions() {
let a = Analyzer::embedded();
Expand Down Expand Up @@ -991,8 +1054,8 @@ End

#[test]
fn loc_variant_reference_suggests_while_typing() {
let src = "Object Tank\n Behavior = TransitionDamageFX ModuleTag_01\n DamagedFXList1 = Loc:X:0.0 Y:0.0 Z:0.0 FXList:FX_\n End\nEnd\n";
let offset = src.find("FXList:FX_").unwrap() + "FXList:FX_".len();
let src = "Object Tank\n Behavior = TransitionDamageFX ModuleTag_01\n ReallyDamagedFXList1 = Loc: X:0 Y:0 Z:0 FXList: FX_\n End\nEnd\n";
let offset = src.find("FXList: FX_").unwrap() + "FXList: FX_".len();
let got = item_with_defs(
src,
offset as u32,
Expand Down
Loading
Loading