From 549315c72c0f59e6daba59864d7ee6e0c4c1782f Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 12 Mar 2026 13:49:47 -0600 Subject: [PATCH 1/2] feat(lsp): auto-enable WordPress stubs for WordPress projects Automatically detect WordPress projects including: - WordPress core installations (wp-config.php, wp-content/, etc.) - WordPress plugins (Plugin Name: header) - WordPress themes (Theme Name: header) - WordPress code patterns (add_action, add_filter, etc.) When detected, automatically enables intelephense.stubs with 'wordpress' for proper function completions and hover information. Fixes #5574 See: https://github.com/bmewburn/vscode-intelephense/issues/1302 --- packages/opencode/src/lsp/server.ts | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 8f93213ea10..548fd29cc2d 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1580,12 +1580,80 @@ export namespace LSPServer { BUN_BE_BUN: "1", }, }) + // Detect WordPress projects (core, themes, or plugins) + const isWordPress = await (async () => { + // Check 1: WordPress core files (for full WP installations) + const wpCoreFiles = [ + "wp-config.php", + "wp-content", + "wp-admin", + "wp-includes", + "wp-settings.php", + "wp-load.php", + ] + for (const wpFile of wpCoreFiles) { + const wpPath = path.join(root, wpFile) + if (await pathExists(wpPath)) { + log.info("WordPress core installation detected", { file: wpFile, root }) + return true + } + } + + // Check 2: WordPress plugin/theme headers in PHP files + // Plugins and themes don't have core files but have specific headers + const phpFiles = await fs.readdir(root).then(files => + files.filter(f => f.endsWith(".php")).map(f => path.join(root, f)) + ).catch(() => []) + + for (const phpFile of phpFiles.slice(0, 10)) { // Check up to 10 PHP files + const content = await fs.readFile(phpFile, "utf-8").catch(() => null) + if (!content) continue + + // WordPress plugin header: Plugin Name: xxx + // WordPress theme header: Theme Name: xxx + // Both use the WordPress PHP header pattern + if ( + /\*\s*Plugin Name:/.test(content) || + /\*\s*Theme Name:/.test(content) || + /\*\s*Text Domain:\s*\S+/.test(content) + ) { + log.info("WordPress plugin/theme detected", { file: path.basename(phpFile), root }) + return true + } + // Check for WordPress function calls in the first 2000 chars + const wpFunctions = [ + "add_action(", + "add_filter(", + "do_action(", + "apply_filters(", + "wp_enqueue_script(", + "wp_enqueue_style(", + "register_activation_hook(", + "register_deactivation_hook(", + "register_uninstall_hook(", + ] + const sample = content.slice(0, 2000) + if (wpFunctions.some(fn => sample.includes(fn))) { + log.info("WordPress code patterns detected", { file: path.basename(phpFile), root }) + return true + } + } + + return false + })() + return { process: proc, initialization: { telemetry: { enabled: false, }, + // Include WordPress stubs for WordPress projects + ...(isWordPress && { + intelephense: { + stubs: ["wordpress"], + }, + }), }, } }, From da5aac8dcdc5507c561736c8430b4cb3fefd3e97 Mon Sep 17 00:00:00 2001 From: David Stone Date: Fri, 13 Mar 2026 11:55:42 -0600 Subject: [PATCH 2/2] fix(lsp): improve WordPress detection and fix stubs configuration - Simplify detection: use filesystem markers only (no PHP content scanning) - Core: wp-config.php, wp-settings.php, wp-load.php - Plugin: main PHP file with 'Plugin Name:' header - Theme: style.css with 'Theme Name:' header - Composer: wordpress/*, wpackagist-*, johnpbloch/wordpress, roots/wordpress - Parent dirs: detect WP core 2-3 levels up (plugins/themes inside wp-content) - Fix stubs: include full Intelephense default stubs list alongside 'wordpress' (setting stubs replaces defaults, so ['wordpress'] alone removes Core, SPL, etc.) - Add diagnostics.undefinedTypes: false to suppress false positives for types from other plugins/packages not covered by stubs (free tier limitation) - Add environment.includePaths pointing to WP core when detected, so Intelephense can resolve WP core classes and functions directly --- packages/opencode/src/lsp/server.ts | 209 ++++++++++++++++++++-------- 1 file changed, 149 insertions(+), 60 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 548fd29cc2d..7dd2ca0bfcd 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1580,80 +1580,169 @@ export namespace LSPServer { BUN_BE_BUN: "1", }, }) - // Detect WordPress projects (core, themes, or plugins) - const isWordPress = await (async () => { - // Check 1: WordPress core files (for full WP installations) - const wpCoreFiles = [ - "wp-config.php", - "wp-content", - "wp-admin", - "wp-includes", - "wp-settings.php", - "wp-load.php", - ] - for (const wpFile of wpCoreFiles) { - const wpPath = path.join(root, wpFile) - if (await pathExists(wpPath)) { - log.info("WordPress core installation detected", { file: wpFile, root }) - return true + // Detect WordPress projects and configure Intelephense accordingly. + // Detection uses cheap filesystem markers only — no PHP content scanning. + const wpDetection: { detected: boolean; hasCore: boolean; corePath?: string } = await (async () => { + // Check 1: WordPress core markers (full installations) + const coreMarkers = ["wp-config.php", "wp-settings.php", "wp-load.php"] + for (const marker of coreMarkers) { + if (await pathExists(path.join(root, marker))) { + log.info("WordPress core detected", { marker, root }) + return { detected: true, hasCore: true } } } - - // Check 2: WordPress plugin/theme headers in PHP files - // Plugins and themes don't have core files but have specific headers - const phpFiles = await fs.readdir(root).then(files => - files.filter(f => f.endsWith(".php")).map(f => path.join(root, f)) - ).catch(() => []) - - for (const phpFile of phpFiles.slice(0, 10)) { // Check up to 10 PHP files - const content = await fs.readFile(phpFile, "utf-8").catch(() => null) - if (!content) continue - - // WordPress plugin header: Plugin Name: xxx - // WordPress theme header: Theme Name: xxx - // Both use the WordPress PHP header pattern - if ( - /\*\s*Plugin Name:/.test(content) || - /\*\s*Theme Name:/.test(content) || - /\*\s*Text Domain:\s*\S+/.test(content) - ) { - log.info("WordPress plugin/theme detected", { file: path.basename(phpFile), root }) - return true + // Also check parent directories — plugins/themes sit inside wp-content/ + for (const depth of ["../..", "../../.."]) { + const candidate = path.resolve(root, depth) + if (await pathExists(path.join(candidate, "wp-load.php"))) { + log.info("WordPress core detected via parent", { candidate, root }) + return { detected: true, hasCore: true, corePath: candidate } } - // Check for WordPress function calls in the first 2000 chars - const wpFunctions = [ - "add_action(", - "add_filter(", - "do_action(", - "apply_filters(", - "wp_enqueue_script(", - "wp_enqueue_style(", - "register_activation_hook(", - "register_deactivation_hook(", - "register_uninstall_hook(", - ] - const sample = content.slice(0, 2000) - if (wpFunctions.some(fn => sample.includes(fn))) { - log.info("WordPress code patterns detected", { file: path.basename(phpFile), root }) - return true + } + + // Check 2: Plugin header — main PHP file with "Plugin Name:" in first 8KB + const mainPluginFile = path.join(root, path.basename(root) + ".php") + const pluginContent = await fs.readFile(mainPluginFile, "utf-8").then((c) => c.slice(0, 8192)).catch(() => null) + if (pluginContent && /\*\s*Plugin Name:/i.test(pluginContent)) { + log.info("WordPress plugin detected", { file: path.basename(mainPluginFile), root }) + return { detected: true, hasCore: false } + } + + // Check 3: Theme header — style.css with "Theme Name:" in first 8KB + const styleContent = await fs.readFile(path.join(root, "style.css"), "utf-8").then((c) => c.slice(0, 8192)).catch(() => null) + if (styleContent && /Theme Name:/i.test(styleContent)) { + log.info("WordPress theme detected", { root }) + return { detected: true, hasCore: false } + } + + // Check 4: composer.json requiring WordPress packages + const composerContent = await fs.readFile(path.join(root, "composer.json"), "utf-8").catch(() => null) + if (composerContent) { + try { + const composer = JSON.parse(composerContent) + const allDeps = { ...composer.require, ...composer["require-dev"] } + const wpPackages = Object.keys(allDeps).some( + (pkg) => pkg.startsWith("wordpress/") || pkg.startsWith("wpackagist-") || pkg === "johnpbloch/wordpress" || pkg === "roots/wordpress", + ) + if (wpPackages) { + log.info("WordPress composer dependency detected", { root }) + return { detected: true, hasCore: false } + } + } catch { + // Invalid JSON — ignore } } - return false + return { detected: false, hasCore: false } })() + // Build Intelephense initialization options for WordPress projects. + // Intelephense includes WordPress stubs by default since v1.12, but setting + // `stubs` replaces the entire default list. We must include all defaults + // alongside "wordpress" to avoid losing standard PHP stubs. + const wpInitialization = wpDetection.detected + ? { + intelephense: { + stubs: [ + // Intelephense defaults (v1.12+) + "apache", + "bcmath", + "bz2", + "calendar", + "com_dotnet", + "Core", + "ctype", + "curl", + "date", + "dba", + "dom", + "enchant", + "exif", + "FFI", + "fileinfo", + "filter", + "fpm", + "ftp", + "gd", + "gettext", + "gmp", + "hash", + "iconv", + "imap", + "intl", + "json", + "ldap", + "libxml", + "mbstring", + "meta", + "mysqli", + "oci8", + "odbc", + "openssl", + "pcntl", + "pcre", + "PDO", + "pdo_ibm", + "pdo_mysql", + "pdo_pgsql", + "pdo_sqlite", + "pgsql", + "Phar", + "posix", + "pspell", + "random", + "readline", + "Reflection", + "regex", + "session", + "shmop", + "SimpleXML", + "snmp", + "soap", + "sockets", + "sodium", + "SPL", + "sqlite3", + "standard", + "superglobals", + "sysvmsg", + "sysvsem", + "sysvshm", + "tidy", + "tokenizer", + "xml", + "xmlreader", + "xmlrpc", + "xmlwriter", + "xsl", + "Zend OPcache", + "zip", + "zlib", + // WordPress + "wordpress", + ], + // Suppress false positives for types from other plugins/packages + // that aren't covered by stubs (especially on Intelephense free tier) + diagnostics: { + undefinedTypes: false, + }, + // Point Intelephense at WordPress core files when available + ...(wpDetection.hasCore && { + environment: { + includePaths: [wpDetection.corePath ?? root], + }, + }), + }, + } + : {} + return { process: proc, initialization: { telemetry: { enabled: false, }, - // Include WordPress stubs for WordPress projects - ...(isWordPress && { - intelephense: { - stubs: ["wordpress"], - }, - }), + ...wpInitialization, }, } },