mod config_index;
mod detect;
mod env_index;
mod location_lookup;
mod route_index;
mod string_call;
mod translation_index;
mod view_index;
pub use config_index::ConfigIndex;
pub use env_index::EnvIndex;
pub use route_index::RouteIndex;
pub use translation_index::TranslationIndex;
pub use view_index::ViewIndex;
use config_index::config_completions;
use env_index::env_completions;
use route_index::route_completions;
use string_call::{call_string_arg, call_string_prefix};
use translation_index::translation_completions;
use view_index::view_completions;
use std::path::Path;
use tower_lsp::lsp_types::{CompletionItem, Location, Position, Url};
pub(crate) use string_call::find_call_sites;
const ENV_CALL_NAMES: &[&str] = &["env"];
const CONFIG_CALL_NAMES: &[&str] = &["config"];
const VIEW_CALL_NAMES: &[&str] = &["view"];
const TRANS_CALL_NAMES: &[&str] = &["__", "trans"];
const ROUTE_CALL_NAMES: &[&str] = &["route"];
#[derive(Debug, Default)]
pub struct LaravelIndex {
pub is_laravel: bool,
pub env: EnvIndex,
pub config: ConfigIndex,
pub views: ViewIndex,
pub translations: TranslationIndex,
pub routes: RouteIndex,
}
impl LaravelIndex {
pub fn load(root: &Path) -> Self {
if !detect::is_laravel_project(root) {
return Self::default();
}
Self {
is_laravel: true,
env: EnvIndex::load(root),
config: ConfigIndex::load(root),
views: ViewIndex::load(root),
translations: TranslationIndex::load(root),
routes: RouteIndex::load(root),
}
}
}
pub(crate) fn resolve_string_key(
source: &str,
position: Position,
laravel: &LaravelIndex,
) -> Option<Location> {
if !laravel.is_laravel {
return None;
}
if let Some((key, _)) = call_string_arg(source, position, ENV_CALL_NAMES) {
return laravel.env.get(&key).cloned();
}
if let Some((key, _)) = call_string_arg(source, position, CONFIG_CALL_NAMES) {
return laravel.config.get(&key).cloned();
}
if let Some((key, _)) = call_string_arg(source, position, VIEW_CALL_NAMES) {
return laravel.views.get(&key).cloned();
}
if let Some((key, _)) = call_string_arg(source, position, TRANS_CALL_NAMES) {
return laravel.translations.get(&key).cloned();
}
if let Some((key, _)) = call_string_arg(source, position, ROUTE_CALL_NAMES) {
return laravel.routes.get(&key).cloned();
}
None
}
pub(crate) fn completions_for_string_key(
source: &str,
position: Position,
laravel: Option<&LaravelIndex>,
) -> Option<Vec<CompletionItem>> {
let laravel = laravel.filter(|l| l.is_laravel)?;
if let Some(prefix) = call_string_prefix(source, position, ENV_CALL_NAMES) {
return Some(env_completions(&laravel.env, &prefix));
}
if let Some(prefix) = call_string_prefix(source, position, CONFIG_CALL_NAMES) {
return Some(config_completions(&laravel.config, &prefix));
}
if let Some(prefix) = call_string_prefix(source, position, VIEW_CALL_NAMES) {
return Some(view_completions(&laravel.views, &prefix));
}
if let Some(prefix) = call_string_prefix(source, position, TRANS_CALL_NAMES) {
return Some(translation_completions(&laravel.translations, &prefix));
}
if let Some(prefix) = call_string_prefix(source, position, ROUTE_CALL_NAMES) {
return Some(route_completions(&laravel.routes, &prefix));
}
None
}
pub(crate) fn resolve_definition_key(
uri: &Url,
position: Position,
laravel: &LaravelIndex,
) -> Option<(&'static [&'static str], String, Location)> {
if !laravel.is_laravel {
return None;
}
if let Some(key) = laravel.env.key_at(uri, position) {
let loc = laravel.env.get(key)?.clone();
return Some((ENV_CALL_NAMES, key.to_string(), loc));
}
if let Some(key) = laravel.config.key_at(uri, position) {
let loc = laravel.config.get(key)?.clone();
return Some((CONFIG_CALL_NAMES, key.to_string(), loc));
}
if let Some(key) = laravel.views.key_at(uri, position) {
let loc = laravel.views.get(key)?.clone();
return Some((VIEW_CALL_NAMES, key.to_string(), loc));
}
if let Some(key) = laravel.translations.key_at(uri, position) {
let loc = laravel.translations.get(key)?.clone();
return Some((TRANS_CALL_NAMES, key.to_string(), loc));
}
if let Some(key) = laravel.routes.key_at(uri, position) {
let loc = laravel.routes.get(key)?.clone();
return Some((ROUTE_CALL_NAMES, key.to_string(), loc));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_non_laravel_root_is_empty_and_inert() {
let tmp = tempfile::tempdir().unwrap();
let idx = LaravelIndex::load(tmp.path());
assert!(!idx.is_laravel);
assert_eq!(idx.env.names().count(), 0);
assert_eq!(idx.config.keys().count(), 0);
assert_eq!(idx.views.names().count(), 0);
assert_eq!(idx.translations.keys().count(), 0);
assert_eq!(idx.routes.names().count(), 0);
}
#[test]
fn load_laravel_root_builds_every_domain_index() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("artisan"), "#!/usr/bin/env php").unwrap();
std::fs::write(tmp.path().join(".env"), "APP_NAME=Test\n").unwrap();
std::fs::create_dir_all(tmp.path().join("config")).unwrap();
std::fs::write(
tmp.path().join("config").join("app.php"),
"<?php\nreturn ['name' => 'Test'];\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("resources").join("views")).unwrap();
std::fs::write(
tmp.path()
.join("resources")
.join("views")
.join("welcome.blade.php"),
"<h1>Hi</h1>",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("lang").join("en")).unwrap();
std::fs::write(
tmp.path().join("lang").join("en").join("auth.php"),
"<?php\nreturn ['failed' => 'Nope'];\n",
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("routes")).unwrap();
std::fs::write(
tmp.path().join("routes").join("web.php"),
"<?php\nRoute::get('/', Foo::class)->name('home');\n",
)
.unwrap();
let idx = LaravelIndex::load(tmp.path());
assert!(idx.is_laravel);
assert!(idx.env.get("APP_NAME").is_some());
assert!(idx.config.get("app.name").is_some());
assert!(idx.views.get("welcome").is_some());
assert!(idx.translations.get("auth.failed").is_some());
assert!(idx.routes.get("home").is_some());
}
#[test]
fn resolve_string_key_none_for_non_laravel() {
let laravel = LaravelIndex::default();
let pos = Position {
line: 0,
character: 12,
};
let src = "<?php\nenv('APP_NAME');\n";
assert!(resolve_string_key(src, pos, &laravel).is_none());
}
#[test]
fn completions_for_string_key_none_when_laravel_is_none() {
let pos = Position {
line: 1,
character: 5,
};
assert!(completions_for_string_key("<?php\nenv('", pos, None).is_none());
}
#[test]
fn resolve_definition_key_finds_env_var_declaration() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("artisan"), "#!/usr/bin/env php").unwrap();
std::fs::write(tmp.path().join(".env"), "APP_NAME=Test\n").unwrap();
let laravel = LaravelIndex::load(tmp.path());
let uri = Url::from_file_path(tmp.path().join(".env")).unwrap();
let (names, key, _loc) = resolve_definition_key(
&uri,
Position {
line: 0,
character: 2,
},
&laravel,
)
.unwrap();
assert_eq!(names, ENV_CALL_NAMES);
assert_eq!(key, "APP_NAME");
}
#[test]
fn resolve_definition_key_none_when_not_on_a_definition() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("artisan"), "#!/usr/bin/env php").unwrap();
std::fs::write(tmp.path().join(".env"), "APP_NAME=Test\n").unwrap();
let laravel = LaravelIndex::load(tmp.path());
let uri = Url::from_file_path(tmp.path().join("app.php")).unwrap();
assert!(
resolve_definition_key(
&uri,
Position {
line: 0,
character: 0
},
&laravel
)
.is_none()
);
}
}