use crate::parsing::LanguageBehavior;
use crate::parsing::ResolutionScope;
use crate::parsing::behavior_state::{BehaviorState, StatefulBehavior};
use crate::parsing::paths::strip_extension;
use crate::parsing::{Import, InheritanceResolver};
use crate::types::compact_string;
use crate::{FileId, Symbol, SymbolKind, Visibility};
use std::path::{Path, PathBuf};
use tree_sitter::Language;
#[derive(Clone)]
pub struct GdscriptBehavior {
language: Language,
state: BehaviorState,
}
impl GdscriptBehavior {
pub fn new() -> Self {
Self {
language: tree_sitter_gdscript::LANGUAGE.into(),
state: BehaviorState::new(),
}
}
fn extract_identifier(signature: &str) -> Option<&str> {
let trimmed = signature.trim();
trimmed
.split([' ', '(', ':', '=', ',', '\t'])
.filter(|token| !token.is_empty())
.find(|token| {
!matches!(
*token,
"func"
| "static"
| "remote"
| "master"
| "puppet"
| "remotesync"
| "mastersync"
| "puppetsync"
| "var"
| "const"
| "signal"
| "class"
| "class_name"
| "export"
| "onready"
| "tool"
)
})
}
fn resolve_gdscript_relative_import(&self, import_path: &str, from_module: &str) -> String {
let mut up_levels = 0;
let mut remaining = import_path;
if let Some(rest) = remaining.strip_prefix("./") {
remaining = rest;
}
while let Some(rest) = remaining.strip_prefix("../") {
up_levels += 1;
remaining = rest;
}
let normalized_from = from_module.strip_prefix("res://").unwrap_or(from_module);
let mut parts: Vec<_> = normalized_from
.split('/')
.filter(|s| !s.is_empty())
.collect();
if !parts.is_empty() {
parts.pop();
}
for _ in 0..up_levels {
if !parts.is_empty() {
parts.pop();
}
}
if !remaining.is_empty() {
let remaining = remaining.strip_suffix(".gd").unwrap_or(remaining);
for part in remaining.split('/') {
if !part.is_empty() {
parts.push(part);
}
}
}
format!("res://{}", parts.join("/"))
}
}
impl StatefulBehavior for GdscriptBehavior {
fn state(&self) -> &BehaviorState {
&self.state
}
}
impl Default for GdscriptBehavior {
fn default() -> Self {
Self::new()
}
}
impl LanguageBehavior for GdscriptBehavior {
fn language_id(&self) -> crate::parsing::registry::LanguageId {
crate::parsing::registry::LanguageId::new("gdscript")
}
fn configure_symbol(&self, symbol: &mut Symbol, module_path: Option<&str>) {
if let Some(path) = module_path {
let full_path = self.format_module_path(path, &symbol.name);
symbol.module_path = Some(full_path.into());
}
if let Some(signature) = &symbol.signature {
symbol.visibility = self.parse_visibility(signature);
}
if symbol.kind == SymbolKind::Module {
if let Some(path) = module_path {
if let Some(name) = path.rsplit('/').next() {
let name = name.trim_end_matches(".gd");
if !name.is_empty() {
symbol.name = compact_string(name);
}
}
}
}
}
fn create_resolution_context(&self, file_id: FileId) -> Box<dyn ResolutionScope> {
Box::new(crate::parsing::gdscript::GdscriptResolutionContext::new(
file_id,
))
}
fn create_inheritance_resolver(&self) -> Box<dyn InheritanceResolver> {
Box::new(crate::parsing::gdscript::GdscriptInheritanceResolver::new())
}
fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String {
base_path.to_string()
}
fn parse_visibility(&self, signature: &str) -> Visibility {
let identifier = Self::extract_identifier(signature).unwrap_or_default();
if identifier.starts_with('_') {
Visibility::Private
} else {
Visibility::Public
}
}
fn module_separator(&self) -> &'static str {
"/"
}
fn format_path_as_module(&self, components: &[&str]) -> Option<String> {
if components.is_empty() {
Some("res://".to_string())
} else {
Some(format!("res://{}", components.join("/")))
}
}
fn module_path_from_file(
&self,
file_path: &Path,
project_root: &Path,
extensions: &[&str],
) -> Option<String> {
let relative = file_path.strip_prefix(project_root).ok()?;
let path = relative.to_string_lossy().replace('\\', "/");
let path_without_ext = strip_extension(&path, extensions);
let normalized = path_without_ext.trim_start_matches('/');
Some(format!("res://{normalized}"))
}
fn get_language(&self) -> Language {
self.language.clone()
}
fn register_file(&self, path: PathBuf, file_id: FileId, module_path: String) {
self.register_file_with_state(path, file_id, module_path);
}
fn add_import(&self, import: Import) {
self.add_import_with_state(import);
}
fn get_imports_for_file(&self, file_id: FileId) -> Vec<Import> {
self.get_imports_from_state(file_id)
}
fn get_module_path_for_file(&self, file_id: FileId) -> Option<String> {
self.state.get_module_path(file_id)
}
fn import_matches_symbol(
&self,
import_path: &str,
symbol_module_path: &str,
importing_module: Option<&str>,
) -> bool {
if import_path == symbol_module_path {
return true;
}
if let Some(importing_mod) = importing_module {
if import_path.starts_with("./") || import_path.starts_with("../") {
let resolved = self.resolve_gdscript_relative_import(import_path, importing_mod);
let norm_symbol = symbol_module_path
.strip_prefix("res://")
.unwrap_or(symbol_module_path)
.strip_suffix(".gd")
.unwrap_or(
symbol_module_path
.strip_prefix("res://")
.unwrap_or(symbol_module_path),
);
let norm_resolved = resolved.strip_prefix("res://").unwrap_or(&resolved);
if norm_resolved == norm_symbol {
return true;
}
}
}
let norm_import = import_path
.strip_prefix("res://")
.unwrap_or(import_path)
.strip_suffix(".gd")
.unwrap_or(import_path.strip_prefix("res://").unwrap_or(import_path));
let norm_symbol = symbol_module_path
.strip_prefix("res://")
.unwrap_or(symbol_module_path)
.strip_suffix(".gd")
.unwrap_or(
symbol_module_path
.strip_prefix("res://")
.unwrap_or(symbol_module_path),
);
norm_import == norm_symbol
}
fn is_resolvable_symbol(&self, symbol: &Symbol) -> bool {
use crate::symbol::ScopeContext;
let resolvable_kind = matches!(
symbol.kind,
SymbolKind::Function
| SymbolKind::Class
| SymbolKind::Variable
| SymbolKind::Constant
| SymbolKind::Method
| SymbolKind::Field );
if !resolvable_kind {
return false;
}
if let Some(ref scope_context) = symbol.scope_context {
matches!(
scope_context,
ScopeContext::Module
| ScopeContext::Global
| ScopeContext::ClassMember { .. }
| ScopeContext::Package
)
} else {
true
}
}
fn is_symbol_visible_from_file(&self, symbol: &Symbol, from_file: FileId) -> bool {
if symbol.file_id == from_file {
return true;
}
let name = symbol.name.as_ref();
if name.starts_with('_') {
return false;
}
true
}
}