Skip to content

Commit 54c8bcb

Browse files
committed
feat: add semantic token modes
Default semantic tokens to a contextual stream so editor syntax grammars remain in charge of ordinary PHP highlighting. Keep the previous broad stream available with full mode and provide an off switch for users who want no LSP semantic highlighting.\n\nDocument the new setting in the configuration reference and schema.
1 parent 6da452b commit 54c8bcb

7 files changed

Lines changed: 579 additions & 104 deletions

File tree

config-schema.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,22 @@
8686
}
8787
}
8888
},
89+
"semantic_tokens": {
90+
"type": "object",
91+
"description": "Controls LSP semantic token highlighting.",
92+
"properties": {
93+
"mode": {
94+
"type": "string",
95+
"description": "Semantic token emission mode. \"contextual\" emits only context-sensitive tokens that complement editor syntax highlighting. \"full\" emits the complete semantic token stream. \"off\" disables semantic tokens.",
96+
"enum": [
97+
"contextual",
98+
"full",
99+
"off"
100+
],
101+
"default": "contextual"
102+
}
103+
}
104+
},
89105
"laravel": {
90106
"type": "object",
91107
"description": "Laravel-specific analysis settings.",

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Semantic token modes.** `.phpantom.toml` now supports `[semantic_tokens] mode = "contextual" | "full" | "off"`. The default `contextual` mode emits only context-sensitive highlighting that complements editor syntax grammars, while `full` keeps the previous broad semantic-token stream and `off` disables semantic tokens. Contributed by @calebdw.
1213
- **`@phpstan-ignore` identifiers are highlighted and completed.** PHPStan ignore comments now highlight the `@phpstan-ignore` tag and each listed error identifier in both docblocks and ordinary `//` comments. Identifier completion works inside the comma-separated ignore list, using PHPStan diagnostic codes already seen in the current file while staying out of per-code parenthesized reasons. Contributed by @calebdw.
1314
- **Parent member override completion.** Typing a method name after `function` in a class body (for example `protected function get`) now suggests public and protected methods from parent classes and interfaces that can still be overridden or implemented, inserting a full signature snippet. The same flow suggests parent properties after `$` (for example `protected $tit`) and parent constants after `const`. Snippets insert `#[\Override]` above methods on PHP 8.3+, properties on PHP 8.5+, and constants on PHP 8.6+ (from `composer.json` / `config.platform.php`). Private members and ones already defined on the class are omitted. Contributed by @calebdw.
1415
- **Reference and implementation count inlay hints.** Classes, interfaces, traits, enums, methods, properties, and constants now show reference counts (e.g. "3 references") as inlay hints. Interfaces and abstract classes additionally show implementation counts (e.g. "2 implementations"). Counts are derived from the cross-file reference index and the go-to-implementation reverse inheritance index, so they are fast even on large codebases. Private members, magic methods, and overridden members are omitted to keep the annotations focused. Contributed by @calebdw.
@@ -42,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4243

4344
### Fixed
4445

46+
- **Deprecated enum cases are recognized.** Enum cases annotated with `@deprecated` or `#[Deprecated]` now carry deprecation metadata, so usages like `self::Low` can be highlighted as deprecated in contextual semantic-token mode. Contributed by @calebdw.
47+
- **Class constant accesses use constant semantic highlighting.** `self::CONSTANT`, `static::CONSTANT`, `parent::CONSTANT`, and `ClassName::CONSTANT` now emit the `enumMember` semantic token instead of being colored as properties, while `ClassName::$property` still emits `property`. Contributed by @calebdw.
4548
- **PHP attributes use decorator semantic highlighting.** Attribute class names in `#[...]` now emit the `decorator` semantic token instead of `class`, so editor themes can color attributes differently from normal class references. Contributed by @calebdw.
4649
- **Go-to-definition on overriding methods jumps to the parent declaration.** When the cursor is on a method definition that overrides a parent or implements an interface method, go-to-definition now navigates to the prototype declaration instead of returning call-site references. Similarly, go-to-definition on a class name jumps to the parent class when one exists. Methods and classes that don't override anything still show usages as before. Contributed by @calebdw.
4750
- **Go-to-definition on overridden properties and constants jumps to the prototype declaration.** Declaration-site navigation now follows overridden properties and constants to the nearest parent or trait declaration, matching the override behavior for methods. Contributed by @calebdw.

docs/configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ message = "^Call to deprecated function some_legacy_helper\\(\\)"
6262
| ---------- | ------ | -------- | ----------- |
6363
| `strategy` | string | `"full"` | Class discovery strategy: `"full"`, `"composer"`, `"self"`, or `"none"`. See [Indexing Strategy](#indexing-strategy) below. |
6464

65+
### `[semantic_tokens]`
66+
67+
PHPantom defaults to `contextual` semantic tokens so editor syntax
68+
highlighting remains in charge of ordinary PHP syntax.
69+
70+
| Key | Type | Default | Description |
71+
| ------ | ------ | -------------- | ----------- |
72+
| `mode` | string | `"contextual"` | Semantic token mode: `"contextual"`, `"full"`, or `"off"`. |
73+
74+
| Mode | Behaviour |
75+
| --- | --- |
76+
| `"contextual"` | Emit only context-sensitive tokens that complement Tree-sitter/TextMate highlighting, such as parameters, PHPDoc template parameters, deprecated references, and static member accesses. |
77+
| `"full"` | Emit the complete semantic token stream, including ordinary classes, variables, functions, methods, properties, comments, keywords, attributes, and Blade tokens. |
78+
| `"off"` | Return no semantic tokens. |
79+
6580
### `[formatting]`
6681

6782
| Key | Type | Default | Description |

src/config.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub struct Config {
2525
pub diagnostics: DiagnosticsConfig,
2626
/// Indexing strategy and file discovery settings.
2727
pub indexing: IndexingConfig,
28+
/// Semantic token highlighting settings.
29+
pub semantic_tokens: SemanticTokensConfig,
2830
/// Formatting proxy settings.
2931
pub formatting: FormattingConfig,
3032
/// PHPStan proxy settings.
@@ -37,6 +39,65 @@ pub struct Config {
3739
pub laravel: LaravelConfig,
3840
}
3941

42+
/// `[semantic_tokens]` section — controls LSP semantic highlighting.
43+
#[derive(Debug, Clone, Default, Deserialize)]
44+
#[serde(default)]
45+
pub struct SemanticTokensConfig {
46+
/// Semantic token emission mode.
47+
///
48+
/// - `"contextual"` (default) — emit only context-sensitive tokens
49+
/// that syntax grammars usually cannot infer.
50+
/// - `"full"` — emit the complete semantic token stream.
51+
/// - `"off"` — return no semantic tokens.
52+
pub mode: Option<SemanticTokensMode>,
53+
}
54+
55+
impl SemanticTokensConfig {
56+
pub fn mode(&self) -> SemanticTokensMode {
57+
self.mode.unwrap_or_default()
58+
}
59+
}
60+
61+
/// Semantic token emission mode.
62+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63+
pub enum SemanticTokensMode {
64+
/// Emit only context-sensitive tokens that complement editor syntax highlighting.
65+
#[default]
66+
Contextual,
67+
/// Emit every token PHPantom can classify.
68+
Full,
69+
/// Disable semantic tokens.
70+
Off,
71+
}
72+
73+
impl<'de> Deserialize<'de> for SemanticTokensMode {
74+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75+
where
76+
D: serde::Deserializer<'de>,
77+
{
78+
let s = String::deserialize(deserializer)?;
79+
match s.as_str() {
80+
"contextual" => Ok(SemanticTokensMode::Contextual),
81+
"full" => Ok(SemanticTokensMode::Full),
82+
"off" => Ok(SemanticTokensMode::Off),
83+
other => Err(serde::de::Error::unknown_variant(
84+
other,
85+
&["contextual", "full", "off"],
86+
)),
87+
}
88+
}
89+
}
90+
91+
impl std::fmt::Display for SemanticTokensMode {
92+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93+
match self {
94+
SemanticTokensMode::Contextual => write!(f, "contextual"),
95+
SemanticTokensMode::Full => write!(f, "full"),
96+
SemanticTokensMode::Off => write!(f, "off"),
97+
}
98+
}
99+
}
100+
40101
#[derive(Debug, Clone, Default, Deserialize)]
41102
#[serde(default)]
42103
pub struct LaravelConfig {
@@ -667,6 +728,10 @@ mod tests {
667728
assert!(!config.diagnostics.report_magic_properties_enabled());
668729
assert!(config.diagnostics.ignore.is_empty());
669730
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
731+
assert_eq!(
732+
config.semantic_tokens.mode(),
733+
SemanticTokensMode::Contextual
734+
);
670735
assert!(config.formatting.php_cs_fixer.is_none());
671736
assert!(config.formatting.phpcbf.is_none());
672737
assert!(config.formatting.timeout.is_none());
@@ -697,6 +762,10 @@ mod tests {
697762
assert!(!config.diagnostics.report_magic_properties_enabled());
698763
assert!(config.diagnostics.ignore.is_empty());
699764
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
765+
assert_eq!(
766+
config.semantic_tokens.mode(),
767+
SemanticTokensMode::Contextual
768+
);
700769
assert!(config.formatting.php_cs_fixer.is_none());
701770
assert!(config.formatting.phpcbf.is_none());
702771
assert!(config.phpstan.command.is_none());
@@ -715,6 +784,10 @@ mod tests {
715784
assert!(!config.diagnostics.extra_arguments_enabled());
716785
assert!(!config.diagnostics.report_magic_properties_enabled());
717786
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
787+
assert_eq!(
788+
config.semantic_tokens.mode(),
789+
SemanticTokensMode::Contextual
790+
);
718791
assert!(config.formatting.php_cs_fixer.is_none());
719792
assert!(config.formatting.phpcbf.is_none());
720793
assert!(config.phpstan.command.is_none());
@@ -959,6 +1032,9 @@ message = "^Call to deprecated function some_legacy_helper\\(\\)"
9591032
[indexing]
9601033
strategy = "self"
9611034
1035+
[semantic_tokens]
1036+
mode = "full"
1037+
9621038
[formatting]
9631039
php-cs-fixer = ""
9641040
phpcbf = "/usr/local/bin/phpcbf"
@@ -996,6 +1072,7 @@ analyze-timeout = 45000
9961072
Some("deprecated_usage")
9971073
);
9981074
assert_eq!(config.indexing.strategy, Some(IndexingStrategy::SelfScan));
1075+
assert_eq!(config.semantic_tokens.mode, Some(SemanticTokensMode::Full));
9991076
assert_eq!(config.formatting.php_cs_fixer.as_deref(), Some(""));
10001077
assert_eq!(
10011078
config.formatting.phpcbf.as_deref(),
@@ -1029,6 +1106,45 @@ analyze-timeout = 45000
10291106
assert_eq!(config.indexing.strategy, Some(IndexingStrategy::Composer));
10301107
}
10311108

1109+
#[test]
1110+
fn parses_semantic_tokens_mode_contextual() {
1111+
let dir = tempfile::tempdir().unwrap();
1112+
let path = dir.path().join(CONFIG_FILE_NAME);
1113+
std::fs::write(&path, "[semantic_tokens]\nmode = \"contextual\"\n").unwrap();
1114+
let config = load_config(dir.path()).unwrap();
1115+
assert_eq!(
1116+
config.semantic_tokens.mode(),
1117+
SemanticTokensMode::Contextual
1118+
);
1119+
}
1120+
1121+
#[test]
1122+
fn parses_semantic_tokens_mode_full() {
1123+
let dir = tempfile::tempdir().unwrap();
1124+
let path = dir.path().join(CONFIG_FILE_NAME);
1125+
std::fs::write(&path, "[semantic_tokens]\nmode = \"full\"\n").unwrap();
1126+
let config = load_config(dir.path()).unwrap();
1127+
assert_eq!(config.semantic_tokens.mode(), SemanticTokensMode::Full);
1128+
}
1129+
1130+
#[test]
1131+
fn parses_semantic_tokens_mode_off() {
1132+
let dir = tempfile::tempdir().unwrap();
1133+
let path = dir.path().join(CONFIG_FILE_NAME);
1134+
std::fs::write(&path, "[semantic_tokens]\nmode = \"off\"\n").unwrap();
1135+
let config = load_config(dir.path()).unwrap();
1136+
assert_eq!(config.semantic_tokens.mode(), SemanticTokensMode::Off);
1137+
}
1138+
1139+
#[test]
1140+
fn invalid_semantic_tokens_mode_returns_parse_error() {
1141+
let dir = tempfile::tempdir().unwrap();
1142+
let path = dir.path().join(CONFIG_FILE_NAME);
1143+
std::fs::write(&path, "[semantic_tokens]\nmode = \"bogus\"\n").unwrap();
1144+
let result = load_config(dir.path());
1145+
assert!(result.is_err());
1146+
}
1147+
10321148
#[test]
10331149
fn parses_laravel_schema_options() {
10341150
let dir = tempfile::tempdir().unwrap();

src/parser/classes.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,17 @@ impl Backend {
14251425
}
14261426
}
14271427
ClassLikeMember::EnumCase(enum_case) => {
1428+
let case_docblock_text = doc_ctx.and_then(|ctx| {
1429+
docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, member)
1430+
});
1431+
let case_docblock_info =
1432+
case_docblock_text.and_then(docblock::parse_docblock_for_tags);
1433+
let depr_info = {
1434+
let docblock_msg = case_docblock_info
1435+
.as_ref()
1436+
.and_then(docblock::extract_deprecation_message_from_info);
1437+
merge_deprecation_info(docblock_msg, &enum_case.attribute_lists, doc_ctx)
1438+
};
14281439
let case_name = atom_bytes(enum_case.item.name().value);
14291440
let case_name_offset = enum_case.item.name().span.start.offset;
14301441
let enum_value = if let EnumCaseItem::Backed(backed) = &enum_case.item {
@@ -1441,8 +1452,8 @@ impl Backend {
14411452
name_offset: case_name_offset,
14421453
type_hint: None,
14431454
visibility: Visibility::Public,
1444-
deprecation_message: None,
1445-
deprecated_replacement: None,
1455+
deprecation_message: depr_info.message,
1456+
deprecated_replacement: depr_info.replacement,
14461457
see_refs: Vec::new(),
14471458
description: None,
14481459
is_enum_case: true,

0 commit comments

Comments
 (0)