Skip to content
Merged
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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

# Quality gate. Runs on every pull request (targeting any branch) and on every
# commit that lands on main. Catches type errors, bundler breakage, grammar
# regressions, and packaging-allowlist mistakes before they reach main.
# release.yml runs separately, only on v* tags.
on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
verify:
name: Type-check, build, test, package
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install dependencies
run: npm ci

# Cheapest / most-likely-to-fail gates first.
- name: Type check
run: npm run check-types

- name: Build (esbuild)
run: npm run build

# Grammar regression tests (vscode-textmate harness over vendored grammars).
- name: Test
run: npm test

# Guard the package.json `files` allowlist (needs dist/ from the build step).
- name: Validate package contents
run: npm run package:check
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Release

# Build the extension and attach the .vsix to the GitHub release for the tag.
# The release is created if it does not exist yet, or updated (asset uploaded)
# if it does.
on:
push:
tags:
- "v*"

# Needed for softprops/action-gh-release to create/update releases and upload assets.
permissions:
contents: write

jobs:
release:
name: Package and publish VSIX
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install dependencies
run: npm ci

# Guard against tagging a release whose tag does not match the manifest
# version: vsce names the file xphp-<package.json version>.vsix, so a
# mismatch would produce a confusingly-named asset. Remove this step if
# you intentionally tag independently of package.json.
- name: Verify tag matches package.json version
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG="$(node -p "require('./package.json').version")"
if [ "$TAG" != "$PKG" ]; then
echo "::error::Tag v$TAG does not match package.json version $PKG"
exit 1
fi

- name: Package extension
run: npm run package

- name: Locate VSIX
id: vsix
run: echo "path=$(ls xphp-*.vsix)" >> "$GITHUB_OUTPUT"

- name: Create or update release and upload VSIX
uses: softprops/action-gh-release@v2
with:
# Pin both the target release and its title to the tag name, so the
# VSIX is always attached to the release for THIS tag (created if it
# does not exist, updated if it does) and the release name == tag name.
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
files: ${{ steps.vsix.outputs.path }}
fail_on_unmatched_files: true
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
out/
*.vsix
.vscode/
docker-compose.override.yml
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Changelog

All notable changes to the **xphp** extension are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0]

First public release. Language support for `.xphp` files powered by the xphp
language server.

### Added
- Syntax highlighting (TextMate grammar) and language configuration for `.xphp`.
- Language-server integration over LSP (stdio) via `vscode-languageclient`.
- Server resolution with a clear priority order:
- `xphp.server.command` — full launch command, used verbatim (e.g. running the
server inside Docker);
- `xphp.server.path` — explicit local PHAR (disables downloading);
- a locally built `./server/xphp-lsp.phar` (`npm run build:server`);
- otherwise **download the pinned release** (`v0.2.4`) from GitHub into global
storage. This is the default — no manual setup required.
- **Checksum verification**: the default download is verified against a built-in
SHA-256 before it is run; a cached PHAR is re-checked on each activation and a
mismatch triggers a re-download. A custom `xphp.server.downloadUrl` can be
verified by also setting `xphp.server.downloadSha256`.
- PHP executable resolution with a non-fatal **PHP 8.4+** version check.
- "Show references" CodeLens handled client-side (`xphp.showReferences`): raw
LSP-JSON arguments are converted to native types and the peek is opened
directly, avoiding the `executeCommandProvider` forwarder that would otherwise
round-trip the click to the server.
- Commands: **Restart Language Server** and **Show Language Server Output**.
- Settings: `xphp.server.command`, `xphp.php.path`, `xphp.server.path`,
`xphp.server.downloadUrl`, `xphp.server.downloadSha256`,
`xphp.server.arguments`, and `xphp.trace.server`.
- Automatic server restart when any server-affecting setting changes.

[0.1.0]: https://github.com/xphp-lang/vscode-extension/releases/tag/v0.1.0
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Development setup

```bash
# 1. Install extension dependencies
npm install

# 2. Build the language server from xphp-lang/language-server@0.2.x.
# Clones it to ../language-server, runs `make build/phar`, and copies the
# result to ./server/xphp-lsp.phar (where the extension looks for it).
npm run build:server

# 3. Build the extension bundle (or use `npm run watch`)
npm run build
```

Then press **F5** in VS Code (the *Run Extension* launch config) to open an
Extension Development Host, and open any `.xphp` file (e.g. `examples/hello.xphp`).

Re-run `npm run build:server` after pulling new server changes, then run the
**xphp: Restart Language Server** command (or reload the window).

### Pointing at a different server checkout

`npm run build:server` honours these environment variables:

| Variable | Default |
| ----------------- | -------------------------------------------------- |
| `XPHP_LSP_REPO` | `https://github.com/xphp-lang/language-server.git` |
| `XPHP_LSP_BRANCH` | `0.2.x` |

Or skip the build entirely and set `xphp.server.path` to an existing PHAR.

## How the server is located

The launch command is resolved in priority order (`start()` in
`src/extension.ts`, then `resolveServer()` in `src/server.ts`):

1. **`xphp.server.command`** — a full launch command used verbatim (first element
the executable, the rest args). Bypasses PHP/PHAR resolution entirely — e.g. to
run the server inside Docker. When set, the steps below are skipped.
2. **`xphp.server.path`** — explicit path to a local `xphp-lsp.phar`; disables
downloading.
3. **`./server/xphp-lsp.phar`** — built locally by `npm run build:server`, or
bundled into a published `.vsix`.
4. **Pinned release download** — the zero-config default. Downloads the
`xphp-lsp.phar` release this build is pinned to (currently `v0.2.4`) into the
extension's global storage and verifies it against a built-in SHA-256. Set
`xphp.server.downloadUrl` to override the source (and `xphp.server.downloadSha256`
to verify it); leave both empty to use the pinned default.

## Settings

| Setting | Default | Description |
| ----------------------------- | ------- | -------------------------------------------------------------------- |
| `xphp.server.command` | `[]` | Full launch command, used verbatim; bypasses PHP/PHAR resolution. |
| `xphp.php.path` | `php` | PHP executable used to run the server (must be 8.4+). |
| `xphp.server.path` | `""` | Explicit path to `xphp-lsp.phar`; overrides resolution. |
| `xphp.server.downloadUrl` | `""` | Override the download source; empty uses the pinned release. |
| `xphp.server.downloadSha256` | `""` | Expected SHA-256 for a custom `downloadUrl` (empty = skip). |
| `xphp.server.arguments` | `[]` | Extra args appended after the PHAR path. |
| `xphp.trace.server` | `off` | Trace LSP traffic (`off` / `messages` / `verbose`). |

## Packaging

```bash
npm run package # produces xphp-<version>.vsix
```

To ship a self-contained build, build the server first and remove the
`server/**` line from `.vscodeignore` so `./server/xphp-lsp.phar` is bundled.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 xphp-lang Matheus Martins

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# vscode-extension
# xphp for VS Code

Language support for **xphp** — a superset of PHP — powered by the
[xphp language server](https://github.com/xphp-lang/language-server).

The extension associates `.xphp` files with the `xphp` language, provides PHP-based
syntax highlighting, and connects to the language server over stdio for diagnostics,
navigation, completion, hover, and code lenses.

## Requirements

- **PHP 8.4+** on your `PATH` (or set `xphp.php.path`). The 0.2.x language server
requires PHP 8.4.
- For local development of the server: `git`, `composer`, and `make`.

18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
# Node toolchain for building the extension — so nothing but VS Code itself
# runs on the host.
#
# docker compose up node # install + watch-rebuild
# docker compose run --rm node npm install # one-off: refresh deps
# docker compose run --rm node npm run build # one-off: single build
# docker compose run --rm node npm run package # one-off: produce the .vsix
#
# node_modules lives in a named volume (not the bind mount) so the container's
# native binaries (esbuild) never clash with the host's. Source and ./dist are
# bind-mounted, so builds land on the host where VS Code's F5 picks them up.
node:
image: node:22
working_dir: /opt/app
volumes:
- ./:/opt/app
command: sh -c "npm install && npm run watch"
27 changes: 27 additions & 0 deletions esbuild.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as esbuild from "esbuild";

const production = process.argv.includes("--production");
const watch = process.argv.includes("--watch");

/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
platform: "node",
// VS Code bundles its own Node; "vscode" is provided at runtime and must
// never be bundled.
external: ["vscode"],
outfile: "dist/extension.js",
sourcemap: !production,
minify: production,
logLevel: "info",
};

if (watch) {
const ctx = await esbuild.context(options);
await ctx.watch();
console.log("[esbuild] watching…");
} else {
await esbuild.build(options);
}
20 changes: 20 additions & 0 deletions examples/hello.xphp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App;

final class Greeter
{
public function __construct(
private readonly string $name,
) {}

public function greet(): string
{
return "Hello, {$this->name}!";
}
}

$greeter = new Greeter('xphp');
echo $greeter->greet(), PHP_EOL;
Binary file added icons/xphp-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions language-configuration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"comments": {
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"]
],
"wordPattern": "(\\$?[A-Za-z_\\x{7f}-\\x{ff}][A-Za-z0-9_\\x{7f}-\\x{ff}]*)",
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"onEnterRules": [
{
"beforeText": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$",
"afterText": "^\\s*\\*/$",
"action": { "indent": "indentOutdent", "appendText": " * " }
},
{
"beforeText": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$",
"action": { "indent": "none", "appendText": " * " }
}
]
}
Loading
Loading