Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Editing intelligence for `.xphp` files inside PhpStorm -- every
capability of the [xphp Language Server](../lsp/) plus a few
PhpStorm-native niceties layered on top. One LSP server, one TextMate
grammar, two editor integrations (this plugin and the [VS Code
PhpStorm-native niceties layered on top. One LSP server, two editor
integrations (this plugin and the [VS Code
extension](../vscode-extension/)).

For what's planned next, see [roadmap](./docs/roadmap.md).
Expand Down Expand Up @@ -32,8 +32,9 @@ In addition to all features supported by the LSP, this plugin provides the
following:

- **Code lens click target** is dispatched client-side
(`editor.action.showReferences`), so clicking the lens lands in
PhpStorm's native usage popup, not a generic LSP location list.
(the server's namespaced `xphp.showReferences` command), so clicking
the lens lands in PhpStorm's native usage popup, not a generic LSP
location list.
- **File rename sync** (the inverse of the LSP `willRenameFiles`
direction) is implemented in plugin Kotlin: a Shift+F6 class
rename triggers the matching file rename via PhpStorm's own
Expand Down Expand Up @@ -100,8 +101,8 @@ Plugin-only Kotlin classes that wrap or extend the standard LSP path:
- `XphpClassRenameListener` (via `BulkFileListener`) -- listens for class
renames inside `.xphp` files and triggers the matching file rename to keep
`PSR-4` in sync.
- `XphpShowReferencesCommandsSupport` -- intercepts
`editor.action.showReferences` from the server and opens
PhpStorm's native usage popup at the lens position.
- `XphpShowReferencesCommandsSupport` -- intercepts the server's
`xphp.showReferences` code-lens command and opens PhpStorm's
native usage popup at the lens position.
- `PharExtractor` -- copies the bundled PHAR from the plugin jar
to PhpStorm's system directory on first load.
10 changes: 0 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,6 @@ dependencies {
// recognised as a sibling.
bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(",") })

// v2 plugin model: extension points declared inside a plugin's
// `<content><module ...>` sub-module aren't reachable through a
// plain `<depends>` on the parent plugin id. The TextMate plugin
// declares `com.intellij.textmate.bundleProvider` and the supporting
// classes under its `intellij.textmate` sub-module, so we need it
// on the compile classpath (mirroring the
// `<dependencies><module name="intellij.textmate"/></dependencies>`
// entry in plugin.xml).
bundledModule("intellij.textmate")

// Toolchain components used by the build / verify pipeline.
pluginVerifier()
zipSigner()
Expand Down
6 changes: 3 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ For LSP-level work see [`../../lsp/docs/roadmap.md`](../../lsp/docs/roadmap.md).

| Surface | Notes |
|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`.xphp` file type recognition** | Bundled TextMate grammar wired through PhpStorm's `FileType` infrastructure. |
| **`.xphp` file type recognition** | `.xphp` files open as plain text and are routed to the LSP by extension; the LSP's semantic tokens supply highlighting. |
| **Zero-config server install** | LSP PHAR bundled inside the plugin jar; `PharExtractor` copies it to PhpStorm's system directory on first plugin load. |
| **All LSP-driven editor features** | Diagnostics, hover, GTD, find usages, completion, rename, code actions, code lens, call / type hierarchy, semantic tokens, signature help, inlay hints, folding ranges, document highlight, document / workspace symbols — see [`README.md#features`](README.md#features). |
| **PSR-4 class ↔ filename rename sync (both directions)** | `XphpFileRenameListener` dispatches LSP 3.17 `willRenameFiles` on VFS moves; `XphpClassRenameListener` triggers the matching file rename when a class is renamed in source. Cross-directory file moves also update the namespace and every consuming `use` import. |
| **Code lens click → native usage popup at lens position** | `XphpShowReferencesCommandsSupport` intercepts `editor.action.showReferences` from the server and anchors PhpStorm's usage chooser to the lens line, not the caret. |
| **Code lens click → native usage popup at lens position** | `XphpShowReferencesCommandsSupport` intercepts the server's `xphp.showReferences` code-lens command and anchors PhpStorm's usage chooser to the lens line, not the caret. |
| **LSP binary override setting** | Preferences → Tools → xPHP → "xphp LSP binary" — for plugin developers iterating against a working-tree `bin/xphp-lsp`. |

---
Expand Down Expand Up @@ -136,7 +136,7 @@ committing to a native action.

### Native Kotlin lexer / parser for `.xphp`

**What it'd do.** Replace the TextMate grammar with a full
**What it'd do.** Replace the LSP-only plain-text model with a full
IntelliJ PSI-aware lexer + parser written in Kotlin, unlocking:
IntelliJ-grade refactoring (extract method / inline / change
signature), structure view that reflects xphp generics natively,
Expand Down
11 changes: 3 additions & 8 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Plugin coordinates surfaced to JetBrains Marketplace when we eventually publish.
pluginGroup = com.xphp.lsp
pluginName = xphp
pluginVersion = 0.1.0
pluginVersion = 0.2.0

# Plugin compatibility window.
#
Expand Down Expand Up @@ -31,18 +31,13 @@ platformVersion = 2026.1.2
# REQUIRED: the build FAILS if this is unset or empty, by design -- a plugin
# jar must never ship without an embedded LSP. Uncomment and set it to the
# phar download URL (e.g. a GitHub release asset):
xphpLspPharUrl = https://github.com/xphp-lang/language-server/releases/download/v0.1.0/xphp-lsp.phar
xphpLspPharUrl = https://github.com/xphp-lang/language-server/releases/download/v0.2.1/xphp-lsp.phar

# Bundled IDE plugin we depend on so PhpStorm's PHP language plumbing is on
# the classpath at compile time and at runtime in the sandbox.
# com.jetbrains.php: PSI hooks for the PHP language family (file type
# discrimination, future XphpLanguage-as-PhpLanguage-dialect work).
# org.jetbrains.plugins.textmate: provides the TextMate bundle / grammar
# APIs (TextMateBundleProvider + `com.intellij.textmate.bundleProvider`
# extension point) that XphpTextMateBundleProvider implements. Without
# it on the compile classpath, that class fails to resolve at build
# time and `.xphp` files open as plain text at runtime.
platformBundledPlugins = com.jetbrains.php,org.jetbrains.plugins.textmate
platformBundledPlugins = com.jetbrains.php

# Toolchain. PhpStorm 2026.x runs on JBR 21; we follow.
javaVersion = 21
Expand Down
3 changes: 1 addition & 2 deletions src/main/kotlin/com/xphp/lsp/PharExtractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ import java.security.MessageDigest
* and the cost is one file write.
*
* Structurally the `@Service` is a thin facade over an inner [Extractor]
* that owns the IO state machine. The split mirrors
* [com.xphp.lsp.textmate.XphpTextMateBundleProvider.Extractor] and exists
* that owns the IO state machine. The split exists
* for testability: unit tests construct the [Extractor] directly with
* caller-controlled `streamLoader` and `targetPath`, so the real production
* state machine is exercised rather than re-implemented under test.
Expand Down
19 changes: 9 additions & 10 deletions src/main/kotlin/com/xphp/lsp/XphpLspServerDescriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,15 @@ class XphpLspServerDescriptor(project: Project) :
// the class (the documented use case), not constructing it from
// outside, and we inherit every default the no-arg path provides.
override val lspCustomization: LspCustomization = object : LspCustomization() {
// Client-side handler for the `editor.action.showReferences`
// command that XphpCodeLensHandler emits with pre-baked
// Location[]. Without this override PhpStorm's default
// LspCommandsSupport round-trips every command to the server
// via `workspace/executeCommand`; our server-side no-op
// returns null, the click silently fails. The override
// intercepts the specific command client-side and navigates
// directly to the first location. See
// XphpShowReferencesCommandsSupport for the rationale and
// multi-location follow-up note.
// Client-side handler for the `xphp.showReferences` command
// that XphpCodeLensHandler emits with pre-baked Location[].
// Without this override PhpStorm's default LspCommandsSupport
// round-trips every code-lens command to the server via
// `workspace/executeCommand`; the server does not register the
// command, so the click would fail. The override intercepts
// the command client-side and navigates directly to the first
// location. See XphpShowReferencesCommandsSupport for the
// rationale and multi-location follow-up note.
override val commandsCustomizer = XphpShowReferencesCommandsSupport()
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/xphp/lsp/XphpLspServerSupportProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import com.intellij.platform.lsp.api.LspServerSupportProvider
* the extension filter here is the docs-recommended pattern
* (https://plugins.jetbrains.com/docs/intellij/language-server-protocol.html
* #basic-implementation) and works regardless of how PhpStorm decides to
* classify `.xphp` files internally (TextMate-handled when our bundle is
* loaded; plain text otherwise).
* classify `.xphp` files internally (plain text, with the LSP's semantic
* tokens supplying highlighting).
*/
class XphpLspServerSupportProvider : LspServerSupportProvider {

Expand Down
40 changes: 21 additions & 19 deletions src/main/kotlin/com/xphp/lsp/XphpShowReferencesCommandsSupport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ import org.eclipse.lsp4j.TextDocumentIdentifier
import javax.swing.JList

/**
* Client-side handler for `editor.action.showReferences` -- the
* de-facto LSP convention for "open the references panel with
* pre-baked locations" emitted by code lenses (and code actions).
* Client-side handler for `xphp.showReferences` -- the namespaced
* command the language server emits on its "Show references" code
* lenses to "open the references panel with pre-baked locations".
*
* PhpStorm's LSP4IJ-rooted LSP adapter doesn't recognize this
* command name out of the box and falls back to a server-side
* `workspace/executeCommand` round-trip. The server we ship
* registers a no-op for the command, so before this customizer
* the click would silently do nothing -- the user's
* `2026-05-30 11:0*` prod log proved exactly that.
* The JetBrains LSP adapter's default `LspCommandsSupport` round-trips
* every code-lens command to the server via `workspace/executeCommand`;
* the server intentionally does NOT register (or advertise) this
* command, so without this customizer the click would error / do
* nothing. (The command is deliberately namespaced rather than the
* VS Code-internal `editor.action.showReferences`: advertising that
* id server-side makes vscode-languageclient shadow VS Code's built-in
* peek, so each client handles a neutral id client-side instead.)
*
* Override here intercepts the command on the client side before
* the round-trip. Dispatch:
Expand Down Expand Up @@ -68,7 +70,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
private fun handleShowReferences(server: LspServer, command: Command) {
val args = command.arguments
if (args == null || args.isEmpty()) {
LOG.debug("editor.action.showReferences: missing arguments")
LOG.debug("xphp.showReferences: missing arguments")
return
}
// Pull the lens-side position out of the command arguments so
Expand All @@ -93,12 +95,12 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
else -> fetchLocations(server, args)
}
if (locations.isEmpty()) {
LOG.debug("editor.action.showReferences: zero locations to navigate to")
LOG.debug("xphp.showReferences: zero locations to navigate to")
return
}
val items = locations.toUsageItems()
if (items.isEmpty()) {
LOG.warn("editor.action.showReferences: every location had an unresolvable URI")
LOG.warn("xphp.showReferences: every location had an unresolvable URI")
return
}
if (items.size == 1) {
Expand All @@ -123,13 +125,13 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
if (args.size < 2) return emptyList()
val uri = parseString(args[0]) ?: run {
LOG.warn(
"editor.action.showReferences: arguments[0] is not a String uri " +
"xphp.showReferences: arguments[0] is not a String uri " +
"(was ${args[0]?.javaClass?.simpleName})"
)
return emptyList()
}
val position = parsePosition(args[1]) ?: run {
LOG.warn("editor.action.showReferences: arguments[1] is not a Position")
LOG.warn("xphp.showReferences: arguments[1] is not a Position")
return emptyList()
}
val params = ReferenceParams(
Expand All @@ -144,7 +146,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
}
raw ?: emptyList()
} catch (e: Exception) {
LOG.warn("editor.action.showReferences: textDocument/references fetch failed", e)
LOG.warn("xphp.showReferences: textDocument/references fetch failed", e)
emptyList()
}
}
Expand Down Expand Up @@ -270,7 +272,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
xy.translate(0, editor.lineHeight)
RelativePoint(editor.contentComponent, xy)
} catch (e: Exception) {
LOG.debug("editor.action.showReferences: anchor-point conversion failed", e)
LOG.debug("xphp.showReferences: anchor-point conversion failed", e)
null
}
}
Expand Down Expand Up @@ -306,7 +308,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
val type = object : TypeToken<List<Location>>() {}.type
gson.fromJson<List<Location>>(json, type)
} catch (e: Exception) {
LOG.warn("editor.action.showReferences: failed to parse locations", e)
LOG.warn("xphp.showReferences: failed to parse locations", e)
null
}
}
Expand Down Expand Up @@ -346,7 +348,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
}

private companion object {
const val COMMAND_NAME = "editor.action.showReferences"
const val COMMAND_NAME = "xphp.showReferences"
private val LOG = Logger.getInstance(XphpShowReferencesCommandsSupport::class.java)

/**
Expand All @@ -364,7 +366,7 @@ class XphpShowReferencesCommandsSupport : LspCommandsSupport() {
val lines = text.split('\n')
if (line >= lines.size) "" else lines[line].trim()
} catch (e: Exception) {
LOG.debug("editor.action.showReferences: could not read preview for ${vfile.url}:$line", e)
LOG.debug("xphp.showReferences: could not read preview for ${vfile.url}:$line", e)
""
}
}
Expand Down
Loading
Loading