Migate PHP Extension to zed-extensions/php (#24583)

PHP Extension has been extracted to it's own repository available here:
- https://github.com/zed-extensions/php
This commit is contained in:
Peter Tripp 2025-02-10 16:07:38 -05:00 committed by GitHub
parent 0af048a7cf
commit 62bb3398ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 0 additions and 905 deletions

View file

@ -1,16 +0,0 @@
[package]
name = "zed_php"
version = "0.2.4"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/php.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View file

@ -1 +0,0 @@
../../LICENSE-APACHE

View file

@ -1,25 +0,0 @@
id = "php"
name = "PHP"
description = "PHP support."
version = "0.2.4"
schema_version = 1
authors = ["Piotr Osiewicz <piotr@zed.dev>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.intelephense]
name = "Intelephense"
language = "PHP"
language_ids = { PHP = "php"}
[language_servers.phpactor]
name = "Phpactor"
language = "PHP"
[grammars.php]
repository = "https://github.com/tree-sitter/tree-sitter-php"
commit = "8ab93274065cbaf529ea15c24360cfa3348ec9e4"
path = "php"
[grammars.phpdoc]
repository = "https://github.com/claytonrcarter/tree-sitter-phpdoc"
commit = "1d0e255b37477d0ca46f1c9e9268c8fa76c0b3fc"

View file

@ -1,4 +0,0 @@
("{" @open "}" @close)
("(" @open ")" @close)
("[" @open "]" @close)
("\"" @open "\"" @close)

View file

@ -1,18 +0,0 @@
name = "PHP"
grammar = "php"
path_suffixes = ["php"]
first_line_pattern = '^#!.*php'
line_comments = ["// ", "# "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
]
collapsed_placeholder = "/* ... */"
word_characters = ["$"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "php"
prettier_plugins = ["@prettier/plugin-php"]

View file

@ -1,36 +0,0 @@
(
(comment)* @context
.
[
(function_definition
"function" @name
name: (_) @name
body: (_
"{" @keep
"}" @keep) @collapse
)
(trait_declaration
"trait" @name
name: (_) @name)
(method_declaration
"function" @name
name: (_) @name
body: (_
"{" @keep
"}" @keep) @collapse
)
(interface_declaration
"interface" @name
name: (_) @name
)
(enum_declaration
"enum" @name
name: (_) @name
)
] @item
)

View file

@ -1,137 +0,0 @@
(php_tag) @tag
"?>" @tag
; Types
(primitive_type) @type.builtin
(cast_type) @type.builtin
(named_type (name) @type) @type
(named_type (qualified_name) @type) @type
; Functions
(array_creation_expression "array" @function.builtin)
(list_literal "list" @function.builtin)
(method_declaration
name: (name) @function.method)
(function_call_expression
function: [(qualified_name (name)) (name)] @function)
(scoped_call_expression
name: (name) @function)
(member_call_expression
name: (name) @function.method)
(function_definition
name: (name) @function)
; Member
(property_element
(variable_name) @property)
(member_access_expression
name: (variable_name (name)) @property)
(member_access_expression
name: (name) @property)
; Variables
(relative_scope) @variable.builtin
((name) @constant
(#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
((name) @constant.builtin
(#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
((name) @constructor
(#match? @constructor "^[A-Z]"))
((name) @variable.builtin
(#eq? @variable.builtin "this"))
(variable_name) @variable
; Basic tokens
[
(string)
(string_value)
(encapsed_string)
(heredoc)
(heredoc_body)
(nowdoc_body)
] @string
(boolean) @constant.builtin
(null) @constant.builtin
(integer) @number
(float) @number
(comment) @comment
"$" @operator
; Keywords
"abstract" @keyword
"and" @keyword
"as" @keyword
"break" @keyword
"callable" @keyword
"case" @keyword
"catch" @keyword
"class" @keyword
"clone" @keyword
"const" @keyword
"continue" @keyword
"declare" @keyword
"default" @keyword
"do" @keyword
"echo" @keyword
"else" @keyword
"elseif" @keyword
"enum" @keyword
"enddeclare" @keyword
"endfor" @keyword
"endforeach" @keyword
"endif" @keyword
"endswitch" @keyword
"endwhile" @keyword
"extends" @keyword
"final" @keyword
"readonly" @keyword
"finally" @keyword
"for" @keyword
"foreach" @keyword
"fn" @keyword
"function" @keyword
"global" @keyword
"goto" @keyword
"if" @keyword
"implements" @keyword
"include_once" @keyword
"include" @keyword
"instanceof" @keyword
"insteadof" @keyword
"interface" @keyword
"match" @keyword
"namespace" @keyword
"new" @keyword
"or" @keyword
"print" @keyword
"private" @keyword
"protected" @keyword
"public" @keyword
"readonly" @keyword
"require_once" @keyword
"require" @keyword
"return" @keyword
"static" @keyword
"switch" @keyword
"throw" @keyword
"trait" @keyword
"try" @keyword
"use" @keyword
"while" @keyword
"xor" @keyword

View file

@ -1 +0,0 @@
(_ "{" "}" @end) @indent

View file

@ -1,11 +0,0 @@
((text) @injection.content
(#set! injection.language "html")
(#set! injection.combined))
((comment) @injection.content
(#match? @injection.content "^/\\*\\*[^*]")
(#set! injection.language "phpdoc"))
((heredoc_body) (heredoc_end) @injection.language) @injection.content
((nowdoc_body) (heredoc_end) @injection.language) @injection.content

View file

@ -1,46 +0,0 @@
(class_declaration
"class" @context
name: (name) @name
) @item
(function_definition
"function" @context
name: (_) @name
) @item
(method_declaration
"function" @context
name: (_) @name
) @item
(interface_declaration
"interface" @context
name: (_) @name
) @item
(enum_declaration
"enum" @context
name: (_) @name
) @item
(trait_declaration
"trait" @context
name: (_) @name
) @item
; Add support for Pest runnable
(function_call_expression
function: (_) @context
(#any-of? @context "it" "test" "describe")
arguments: (arguments
.
(argument
[
(encapsed_string (string_value) @name)
(string (string_value) @name)
]
)
)
) @item
(comment) @annotation

View file

@ -1,105 +0,0 @@
; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier
; and have a method that follow the naming convention of PHPUnit test methods
; and the method is public
(
(class_declaration
modifier: (_)? @_modifier
(#not-eq? @_modifier "abstract")
name: (_) @_name
(#match? @_name ".*Test$")
body: (declaration_list
(method_declaration
(visibility_modifier)? @_visibility
(#eq? @_visibility "public")
name: (_) @run
(#match? @run "^test.*")
)
)
) @_phpunit-test
(#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier
; and have a method that has the @test annotation
; and the method is public
(
(class_declaration
modifier: (_)? @_modifier
(#not-eq? @_modifier "abstract")
name: (_) @_name
(#match? @_name ".*Test$")
body: (declaration_list
((comment) @_comment
(#match? @_comment ".*@test\\b.*")
.
(method_declaration
(visibility_modifier)? @_visibility
(#eq? @_visibility "public")
name: (_) @run
(#not-match? @run "^test.*")
))
)
) @_phpunit-test
(#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier
; and have a method that has the #[Test] attribute
; and the method is public
(
(class_declaration
modifier: (_)? @_modifier
(#not-eq? @_modifier "abstract")
name: (_) @_name
(#match? @_name ".*Test$")
body: (declaration_list
(method_declaration
(attribute_list
(attribute_group
(attribute (name) @_attribute)
)
)
(#eq? @_attribute "Test")
(visibility_modifier)? @_visibility
(#eq? @_visibility "public")
name: (_) @run
(#not-match? @run "^test.*")
)
)
) @_phpunit-test
(#set! tag phpunit-test)
)
; Class that follow the naming convention of PHPUnit test classes
; and that doesn't have the abstract modifier
(
(class_declaration
modifier: (_)? @_modifier
(#not-eq? @_modifier "abstract")
name: (_) @run
(#match? @run ".*Test$")
) @_phpunit-test
(#set! tag phpunit-test)
)
; Add support for Pest runnable
; Function expression that has `it`, `test` or `describe` as the function name
(
(function_call_expression
function: (_) @_name
(#any-of? @_name "it" "test" "describe")
arguments: (arguments
.
(argument
[
(encapsed_string (string_value) @run)
(string (string_value) @run)
]
)
)
) @_pest-test
(#set! tag pest-test)
)

View file

@ -1,40 +0,0 @@
(namespace_definition
name: (namespace_name) @name) @module
(interface_declaration
name: (name) @name) @definition.interface
(trait_declaration
name: (name) @name) @definition.interface
(class_declaration
name: (name) @name) @definition.class
(class_interface_clause [(name) (qualified_name)] @name) @impl
(property_declaration
(property_element (variable_name (name) @name))) @definition.field
(function_definition
name: (name) @name) @definition.function
(method_declaration
name: (name) @name) @definition.function
(object_creation_expression
[
(qualified_name (name) @name)
(variable_name (name) @name)
]) @reference.class
(function_call_expression
function: [
(qualified_name (name) @name)
(variable_name (name)) @name
]) @reference.call
(scoped_call_expression
name: (name) @name) @reference.call
(member_call_expression
name: (name) @name) @reference.call

View file

@ -1,19 +0,0 @@
[
{
"label": "phpunit test $ZED_SYMBOL",
"command": "./vendor/bin/phpunit",
"args": ["--filter $ZED_SYMBOL $ZED_FILE"],
"tags": ["phpunit-test"]
},
{
"label": "pest test $ZED_SYMBOL",
"command": "./vendor/bin/pest",
"args": ["--filter \"$ZED_SYMBOL\" $ZED_FILE"],
"tags": ["pest-test"]
},
{
"label": "execute selection $ZED_SELECTED_TEXT",
"command": "php",
"args": ["-r", "$ZED_SELECTED_TEXT"]
}
]

View file

@ -1,45 +0,0 @@
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(method_declaration
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(method_declaration) @function.around
(class_declaration
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(interface_declaration
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(trait_declaration
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(enum_declaration
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(namespace_definition
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(comment)+ @comment.around

View file

@ -1,9 +0,0 @@
name = "PHPDoc"
grammar = "phpdoc"
autoclose_before = "]})>"
brackets = [
{ start = "{", end = "}", close = true, newline = false },
{ start = "[", end = "]", close = true, newline = false },
{ start = "(", end = ")", close = true, newline = false },
{ start = "<", end = ">", close = true, newline = false },
]

View file

@ -1,13 +0,0 @@
(tag_name) @keyword
(tag (variable_name) @variable)
(variable_name "$" @operator)
(tag
(tag_name) @keyword
(#eq? @keyword "@method")
(name) @function.method)
(primitive_type) @type.builtin
(named_type (name) @type) @type
(named_type (qualified_name) @type) @type

View file

@ -1,5 +0,0 @@
mod intelephense;
mod phpactor;
pub use intelephense::*;
pub use phpactor::*;

View file

@ -1,209 +0,0 @@
use std::{env, fs};
use zed::{CodeLabel, CodeLabelSpan};
use zed_extension_api::settings::LspSettings;
use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
const SERVER_PATH: &str = "node_modules/intelephense/lib/intelephense.js";
const PACKAGE_NAME: &str = "intelephense";
pub struct Intelephense {
did_find_server: bool,
}
impl Intelephense {
pub const LANGUAGE_SERVER_ID: &'static str = "intelephense";
pub fn new() -> Self {
Self {
did_find_server: false,
}
}
pub fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
if let Some(path) = worktree.which("intelephense") {
return Ok(zed::Command {
command: path,
args: vec!["--stdio".to_string()],
env: Default::default(),
});
}
let server_path = self.server_script_path(language_server_id)?;
Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![
env::current_dir()
.unwrap()
.join(&server_path)
.to_string_lossy()
.to_string(),
"--stdio".to_string(),
],
env: Default::default(),
})
}
fn server_exists(&self) -> bool {
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
}
fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
let server_exists = self.server_exists();
if self.did_find_server && server_exists {
return Ok(SERVER_PATH.to_string());
}
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
if !server_exists
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
{
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
let result = zed::npm_install_package(PACKAGE_NAME, &version);
match result {
Ok(()) => {
if !self.server_exists() {
Err(format!(
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
))?;
}
}
Err(error) => {
if !self.server_exists() {
Err(error)?;
}
}
}
}
self.did_find_server = true;
Ok(SERVER_PATH.to_string())
}
pub fn language_server_workspace_configuration(
&mut self,
worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
let settings = LspSettings::for_worktree("intelephense", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.settings.clone())
.unwrap_or_default();
Ok(Some(serde_json::json!({
"intelephense": settings
})))
}
pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option<CodeLabel> {
let label = &completion.label;
match completion.kind? {
zed::lsp::CompletionKind::Method => {
// __construct method doesn't have a detail
if let Some(ref detail) = completion.detail {
if detail.is_empty() {
return Some(CodeLabel {
spans: vec![
CodeLabelSpan::literal(label, Some("function.method".to_string())),
CodeLabelSpan::literal("()", None),
],
filter_range: (0..label.len()).into(),
code: completion.label,
});
}
}
let mut parts = completion.detail.as_ref()?.split(":");
// E.g., `foo(string $var)`
let name_and_params = parts.next()?;
let return_type = parts.next()?.trim();
let (_, params) = name_and_params.split_once("(")?;
let params = params.trim_end_matches(")");
Some(CodeLabel {
spans: vec![
CodeLabelSpan::literal(label, Some("function.method".to_string())),
CodeLabelSpan::literal("(", None),
CodeLabelSpan::literal(params, Some("comment".to_string())),
CodeLabelSpan::literal("): ", None),
CodeLabelSpan::literal(return_type, Some("type".to_string())),
],
filter_range: (0..label.len()).into(),
code: completion.label,
})
}
zed::lsp::CompletionKind::Constant | zed::lsp::CompletionKind::EnumMember => {
if let Some(ref detail) = completion.detail {
if !detail.is_empty() {
return Some(CodeLabel {
spans: vec![
CodeLabelSpan::literal(label, Some("constant".to_string())),
CodeLabelSpan::literal(" ", None),
CodeLabelSpan::literal(detail, Some("comment".to_string())),
],
filter_range: (0..label.len()).into(),
code: completion.label,
});
}
}
Some(CodeLabel {
spans: vec![CodeLabelSpan::literal(label, Some("constant".to_string()))],
filter_range: (0..label.len()).into(),
code: completion.label,
})
}
zed::lsp::CompletionKind::Property => {
let return_type = completion.detail?;
Some(CodeLabel {
spans: vec![
CodeLabelSpan::literal(label, Some("attribute".to_string())),
CodeLabelSpan::literal(": ", None),
CodeLabelSpan::literal(return_type, Some("type".to_string())),
],
filter_range: (0..label.len()).into(),
code: completion.label,
})
}
zed::lsp::CompletionKind::Variable => {
// See https://www.php.net/manual/en/reserved.variables.php
const SYSTEM_VAR_NAMES: &[&str] =
&["argc", "argv", "php_errormsg", "http_response_header"];
let var_name = completion.label.trim_start_matches("$");
let is_uppercase = var_name
.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_uppercase());
let is_system_constant = var_name.starts_with("_");
let is_reserved = SYSTEM_VAR_NAMES.contains(&var_name);
let highlight = if is_uppercase || is_system_constant || is_reserved {
Some("comment".to_string())
} else {
None
};
Some(CodeLabel {
spans: vec![CodeLabelSpan::literal(label, highlight)],
filter_range: (0..label.len()).into(),
code: completion.label,
})
}
_ => None,
}
}
}

View file

@ -1,85 +0,0 @@
use std::fs;
use zed_extension_api::{self as zed, LanguageServerId, Result};
pub struct Phpactor {
cached_binary_path: Option<String>,
}
impl Phpactor {
pub const LANGUAGE_SERVER_ID: &'static str = "phpactor";
pub fn new() -> Self {
Self {
cached_binary_path: None,
}
}
pub fn language_server_binary_path(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<String> {
if let Some(path) = worktree.which("phpactor") {
return Ok(path);
}
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
return Ok(path.clone());
}
}
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let release = zed::latest_github_release(
"phpactor/phpactor",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
)?;
let asset_name = "phpactor.phar";
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
let version_dir = format!("phpactor-{}", release.version);
fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?;
let binary_path = format!("{version_dir}/phpactor.phar");
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
zed::download_file(
&asset.download_url,
&binary_path,
zed::DownloadedFileType::Uncompressed,
)
.map_err(|e| format!("failed to download file: {e}"))?;
zed::make_file_executable(&binary_path)?;
let entries =
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
if entry.file_name().to_str() != Some(&version_dir) {
fs::remove_dir_all(entry.path()).ok();
}
}
}
self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
}
}

View file

@ -1,72 +0,0 @@
mod language_servers;
use zed::CodeLabel;
use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
use crate::language_servers::{Intelephense, Phpactor};
struct PhpExtension {
intelephense: Option<Intelephense>,
phpactor: Option<Phpactor>,
}
impl zed::Extension for PhpExtension {
fn new() -> Self {
Self {
intelephense: None,
phpactor: None,
}
}
fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
match language_server_id.as_ref() {
Intelephense::LANGUAGE_SERVER_ID => {
let intelephense = self.intelephense.get_or_insert_with(Intelephense::new);
intelephense.language_server_command(language_server_id, worktree)
}
Phpactor::LANGUAGE_SERVER_ID => {
let phpactor = self.phpactor.get_or_insert_with(Phpactor::new);
Ok(zed::Command {
command: phpactor.language_server_binary_path(language_server_id, worktree)?,
args: vec!["language-server".into()],
env: Default::default(),
})
}
language_server_id => Err(format!("unknown language server: {language_server_id}")),
}
}
fn language_server_workspace_configuration(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
if language_server_id.as_ref() == Intelephense::LANGUAGE_SERVER_ID {
if let Some(intelephense) = self.intelephense.as_mut() {
return intelephense.language_server_workspace_configuration(worktree);
}
}
Ok(None)
}
fn label_for_completion(
&self,
language_server_id: &zed::LanguageServerId,
completion: zed::lsp::Completion,
) -> Option<CodeLabel> {
match language_server_id.as_ref() {
Intelephense::LANGUAGE_SERVER_ID => {
self.intelephense.as_ref()?.label_for_completion(completion)
}
_ => None,
}
}
}
zed::register_extension!(PhpExtension);