Initial Commit

master
mtresearchdev 2023-06-15 11:22:53 +01:00 committed by m2researchdev
commit 639b0f451d
30 changed files with 6761 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
out/

30
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.2.0",
"configurations": [
{
"type": "extensionHost",
"request": "launch",
"name": "Launch Client",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"outFiles": ["${workspaceRoot}/client/out/**/*.js"],
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
},
{
"name": "Language Server E2E Test",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/client/out/test/index",
"${workspaceRoot}/client/testFixture"
],
"outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}
]
}

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

33
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,33 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"group": "build",
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": [
"$tsc"
]
},
{
"type": "npm",
"script": "watch",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": [
"$tsc-watch"
]
}
]
}

4
.vscodeignore Normal file
View File

@ -0,0 +1,4 @@
.vscode/**
.vscode-test/**
.gitignore
vsc-extension-quickstart.md

28
CHANGELOG.md Normal file
View File

@ -0,0 +1,28 @@
# Change Log
All notable changes to the "openquest" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
---

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# OpenQuest
A Language extension for the Quest language, a DSL for mission scripting (Metin2)
## Features
Supports mostly all known tipical features like:
- Quest Language Highlighting and formatting
- Lua Language embedded in respective Quest scopes
- [OpenQuest Language Server support](https://github.com/oridevteam/openquest-rs)
- Language Server connections (local and remote)
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
The extension is able to provide everything necessary by itself,
however you can check its Settings for things you might want to change to your own
## Known Issues
Nothing quite yet

23
client/package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "openquest-lsp-client",
"description": "OpenQuest VSC LSP CLient",
"author": "openquest",
"license": "MIT",
"version": "0.0.1",
"publisher": "vscode",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-extension-samples"
},
"engines": {
"vscode": "^1.75.0"
},
"dependencies": {
"vscode-languageclient": "^8.1.0"
},
"devDependencies": {
"@types/node": "^20.2.5",
"@types/vscode": "^1.75.1",
"@vscode/test-electron": "^2.2.3"
}
}

354
client/pnpm-lock.yaml Normal file
View File

@ -0,0 +1,354 @@
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
vscode-languageclient:
specifier: ^8.1.0
version: 8.1.0
devDependencies:
'@types/node':
specifier: ^20.2.5
version: 20.2.5
'@types/vscode':
specifier: ^1.75.1
version: 1.75.1
'@vscode/test-electron':
specifier: ^2.2.3
version: 2.2.3
packages:
/@tootallnate/once@1.1.2:
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
engines: {node: '>= 6'}
dev: true
/@types/node@20.2.5:
resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==}
dev: true
/@types/vscode@1.75.1:
resolution: {integrity: sha512-emg7wdsTFzdi+elvoyoA+Q8keEautdQHyY5LNmHVM4PTpY8JgOTVADrGVyXGepJ6dVW2OS5/xnLUWh+nZxvdiA==}
dev: true
/@vscode/test-electron@2.2.3:
resolution: {integrity: sha512-7DmdGYQTqRNaLHKG3j56buc9DkstriY4aV0S3Zj32u0U9/T0L8vwWAC9QGCh1meu1VXDEla1ze27TkqysHGP0Q==}
engines: {node: '>=16'}
dependencies:
http-proxy-agent: 4.0.1
https-proxy-agent: 5.0.1
rimraf: 3.0.2
unzipper: 0.10.14
transitivePeerDependencies:
- supports-color
dev: true
/agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/big-integer@1.6.51:
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
engines: {node: '>=0.6'}
dev: true
/binary@0.3.0:
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
dev: true
/bluebird@3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
dev: true
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: true
/brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: false
/buffer-indexof-polyfill@1.0.2:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
dev: true
/buffers@0.1.1:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
dev: true
/chainsaw@0.1.0:
resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
dependencies:
traverse: 0.3.9
dev: true
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: true
/duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
dependencies:
readable-stream: 2.3.8
dev: true
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
/fstream@1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.7.1
dev: true
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: true
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
/http-proxy-agent@4.0.1:
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
engines: {node: '>= 6'}
dependencies:
'@tootallnate/once': 1.1.2
agent-base: 6.0.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: true
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true
/listenercount@1.0.1:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
dev: true
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: true
/minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: false
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: true
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: true
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
dev: true
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: true
/rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
hasBin: true
dependencies:
glob: 7.2.3
dev: true
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true
dependencies:
glob: 7.2.3
dev: true
/safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: true
/semver@7.5.1:
resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
dev: true
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
dev: true
/traverse@0.3.9:
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
dev: true
/unzipper@0.10.14:
resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
dependencies:
big-integer: 1.6.51
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
dev: true
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/vscode-jsonrpc@8.1.0:
resolution: {integrity: sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==}
engines: {node: '>=14.0.0'}
dev: false
/vscode-languageclient@8.1.0:
resolution: {integrity: sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==}
engines: {vscode: ^1.67.0}
dependencies:
minimatch: 5.1.6
semver: 7.5.1
vscode-languageserver-protocol: 3.17.3
dev: false
/vscode-languageserver-protocol@3.17.3:
resolution: {integrity: sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==}
dependencies:
vscode-jsonrpc: 8.1.0
vscode-languageserver-types: 3.17.3
dev: false
/vscode-languageserver-types@3.17.3:
resolution: {integrity: sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==}
dev: false
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false

View File

@ -0,0 +1,275 @@
// Local Imports
import constants = require("./constants");
import { clients, outputChannel } from "./extension";
import * as server_binary from "./server_binary";
// External Imports
import { Duplex } from "stream";
import WebSocket, { createWebSocketStream } from 'ws';
import {
ExtensionContext, RelativePattern, WorkspaceFolder,
WorkspaceFoldersChangeEvent, workspace
} from "vscode";
import * as vscode from "vscode";
import { LanguageClient, StreamInfo, integer } from "vscode-languageclient/node";
import { ServerOptions } from 'vscode-languageclient/node';
import { TextDocument, Uri } from 'vscode';
/**
*
*/
async function openDocument(uri: Uri) {
const uriMatch = (d: TextDocument) => d.uri.toString() === uri.toString();
const doc = vscode.workspace.textDocuments.find(uriMatch);
if (doc === undefined) await vscode.workspace.openTextDocument(uri);
return uri;
}
/**
*
*/
async function openPestFilesInFolder(folder: Uri) {
const pattern = filesInFolderPattern(folder);
const uris = await workspace.findFiles(pattern);
return Promise.all(uris.map(openDocument));
}
/**
*
*/
export function filesInFolderPattern(folder: Uri) {
return new RelativePattern(folder, constants.DOCUMENT_EXTENSION_PATTERN);
}
/**
*
*/
export async function startClientsForFolder(folder: WorkspaceFolder, ctx: ExtensionContext) {
// Settings on how to setup and connect the server
let serverOptions = await makeServerOptions();
if (!serverOptions) {
outputChannel.appendLine("[ERROR] Aborting server start.");
/*
await window.showErrorMessage(
"Not starting Pest Language Server as a suitable binary was not found."
);
*/
return;
}
const root = folder.uri;
const deleteWatcher = workspace.createFileSystemWatcher(
filesInFolderPattern(root),
true, // ignoreCreateEvents
true, // ignoreChangeEvents
false // ignoreDeleteEvents
);
const createChangeWatcher = workspace.createFileSystemWatcher(
filesInFolderPattern(root),
false, // ignoreCreateEvents
false, // ignoreChangeEvents
true // ignoreDeleteEvents
);
ctx.subscriptions.push(deleteWatcher);
ctx.subscriptions.push(createChangeWatcher);
const client = new LanguageClient(
constants.EXTENSION_NAME,
serverOptions,
{
documentSelector: [
{
language: constants.LANGUAGE_ID,
pattern: `${root.fsPath}/${constants.DOCUMENT_EXTENSION_PATTERN}`
},
],
synchronize: { fileEvents: deleteWatcher },
diagnosticCollectionName: constants.EXTENSION_NAME,
workspaceFolder: folder,
outputChannel,
}
);
client.start(); // ctx.subscriptions.push(client.start());
ctx.subscriptions.push(createChangeWatcher.onDidCreate(openDocument));
ctx.subscriptions.push(createChangeWatcher.onDidChange(openDocument));
const openedFiles = await openPestFilesInFolder(root);
const pestFiles: Set<string> = new Set();
openedFiles.forEach(f => pestFiles.add(f.toString()));
clients.set(root.toString(), client);
}
/**
* Makes Language Server options based on workspace settings, it might:
* - Try to find where a executable path is in package managers install paths
* - Find a given executable by path in given workspace settings
* - Connect to an existing language server websocket on an address
*/
async function makeServerOptions(): Promise<ServerOptions | undefined> {
const workspaceConfig = vscode.workspace.getConfiguration(constants.SETTINGS_NAMESPACE);
const customArgs = workspaceConfig.get("serverArguments") as string[];
// Make either a configuration to spawn a server locally
// or connect to an existing local or remote one
switch (workspaceConfig.get("handlingMode")) {
// Tries to find a language server executable from known package managers
// and spawns it
case "Automatic (Package Manager)":
const serverPath = await server_binary.findLanguageServerPath();
const automaticOptions: ServerOptions = {
command: serverPath, args: customArgs
};
return automaticOptions;
// Finds specified language server in path and spawns it
case "Local Path":
const command: string = workspaceConfig.get("serverBinaryPath");
let serverOptions: ServerOptions = {
command, args: customArgs
};
return serverOptions;
// Connect to a websocket by specified address in settings
case "Remote/Local Connection":
const addr: string = workspaceConfig.get("serverAddress");
return websocketServerOptions(addr);
default:
vscode.window.showErrorMessage("[ERROR] Something that shouldn't error, errored!")
break;
}
return undefined;
}
/**
*
*/
function ipcSocketServerOptions(pid: number) {
var child_process = require("child_process");
}
/**
*
* @param pid
* @returns
*/
function pipeLanguageServer(pid: number): StreamInfo {
var child_process = require("child_process");
// Obtain the stdin and stdout streams of the preexisting process using its PID
const languageServerProcess = child_process.spawn('sh', ['-c', `exec ${pid}>&0 <&${pid} 2>&1`], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});
// Create a StreamInfo object with the input/output streams of the language server process
const streamInfo: StreamInfo = {
writer: languageServerProcess.stdin,
reader: languageServerProcess.stdout
};
return streamInfo;
}
/**
* Generaters server options with a websocket ready connection
* @param address
*/
function websocketServerOptions(address: string): ServerOptions {
function connectToServer(hostname: string): Duplex {
const socket = new WebSocket(`ws://${hostname}/`);
const stream = createWebSocketStream(socket);
stream.on('error', (e) => {
outputChannel.appendLine(`[WS] Could not connect: ${e.message}`);
});
stream.on('open', (e) => outputChannel.appendLine(`[WS] Connected: ${e.message}`))
stream.on("data", function (chunk) {
console.log(new TextDecoder().decode(chunk));
});
if (socket && (socket.readyState === WebSocket.CONNECTING)) {
// TODO: Delay execution while socket is connecting for a bit
}
if (socket && (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.CLOSING)) {
outputChannel.append("[WS] Not connected:")
return undefined;
}
return stream;
}
let connection = connectToServer(address);
return () => Promise.resolve({
reader: connection,
writer: connection,
});
}
/**
*
* @param client
* @returns
*/
export function stopClient(client: LanguageClient) {
client.diagnostics?.clear();
return client.stop();
}
/**
*
* @param workspaceFolder
*/
export async function stopClientsForFolder(workspaceFolder: string) {
const client = clients.get(workspaceFolder);
if (client) {
await stopClient(client);
}
clients.delete(workspaceFolder);
}
/**
*
*/
export function updateClients(context: ExtensionContext) {
return async function ({ added, removed }: WorkspaceFoldersChangeEvent) {
for (const folder of removed) {
await stopClientsForFolder(folder.uri.toString());
}
for (const folder of added) {
await startClientsForFolder(folder, context);
}
};
}

View File

@ -0,0 +1,67 @@
//
/*
function connectToServer(hostname: string, path: string): Duplex {
const ws = new WebSocket(`ws://${hostname}/${path}`);
return WebSocket.createWebSocketStream(ws);
}
*/
////function makeClient() {
// // Options to control the language client
// const clientOptions: LanguageClientOptions = {
// // Register the server for plain text documents
// documentSelector: [{ scheme: 'file', language: 'plaintext' }],
// synchronize: {
// // Notify the server about file changes to '.clientrc files contained in the workspace
// fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
// }
// };
//
// // The server is implemented in node
// const serverModule = context.asAbsolutePath(
// path.join('server', 'out', 'server.js')
// );
//
// // Create the language client and start it
// client = new LanguageClient(
// 'OpenQuestLSP',
// 'OpenQuest Language Server',
// serverOptionsGenerator(),
// clientOptions
// );
//
// // The client will either connect or launch the language server, depending
// // on the workspace settings
// client.start();
//}
//
//
//
///**
// * Make server options based on the workspace configuration
// */
//function serverOptionsGenerator(): ServerOptions {
// if (vscode.workspace.getConfiguration(constants.SETTINGS_NAMESPACE).get("serverAddress") !== null) {
// // If the extension is launched in debug mode then the debug server options are used
// // Otherwise the run options are used
// return {
// run: { module: serverModule, transport: TransportKind.ipc },
// debug: {
// module: serverModule,
// transport: TransportKind.ipc,
// }
// };
// } else {
// return {
// run: { module: serverModule, transport: TransportKind.ipc },
// debug: {
// module: serverModule,
// transport: TransportKind.ipc,
// }
// };
// }
//}
//*/

18
client/src/constants.ts Normal file
View File

@ -0,0 +1,18 @@
// TODO: Check if some of these things can be accessed trough legacy code
// since they are in `package.json`, which could be accessible somehow
/** Name of the VSCode extension namespace */
export const DISPLAY_NAME: string = "OpenQuest"
export const EXTENSION_NAME: string = "openquest"
export const LANGUAGE_ID: string = "quest"
export const SETTINGS_NAMESPACE: string = EXTENSION_NAME
export const SUPPORTED_EXTENSIONS = {
};
export const DOCUMENT_EXTENSION_PATTERN = "**/*.quest";
export const documentsPerExtensionInPath = (path) => `${path}/${DOCUMENT_EXTENSION_PATTERN}`;

87
client/src/extension.ts Normal file
View File

@ -0,0 +1,87 @@
// Local Imports
import constants = require('./constants');
import * as client_handler from "./client_handler"
// External Imports
import { workspace, ExtensionContext } from 'vscode';
import * as vscode from "vscode";
import { LanguageClient } from 'vscode-languageclient/node';
import { startClientsForFolder } from './client_handler';
// Common and Persistent variables
export const outputChannel = vscode.window.createOutputChannel(
constants.DISPLAY_NAME, constants.LANGUAGE_ID
);
export const clients: Map<string, LanguageClient> = new Map();
/**
* This function gets called when the extension is activated,
* depending on what is the trigger configuration on `package.json#activationEvents
*/
export function activate(context: ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
outputChannel.appendLine('[INFO] Extension is now active, good deving and have fun !');
// Commands (these are defined in `package.json#contributes.configurations`)
context.subscriptions.push(
vscode.commands.registerCommand(`${constants.SETTINGS_NAMESPACE}.reload_server`, () => {
// The code placed here will be executed every time this command is ran
vscode.window.showInformationMessage(`[INFO] Reloading Language Server`);
})
);
// Start Language Clients for each folder in the workspace
for (const folder of workspace.workspaceFolders || []) {
startClientsForFolder(folder, context);
}
// Client to Server synchronization
context.subscriptions.push(
workspace.onDidChangeWorkspaceFolders(client_handler.updateClients(context))
);
// Refresh client and server settings when changes happen
context.subscriptions.push(workspace.onDidChangeConfiguration(
(event) => refreshSettings(event, context))
);
}
export async function deactivate(): Promise<void> {
await Promise.all([...clients.values()].map(client_handler.stopClient));
}
/**
* Checks what changes were made in workspace and user settings and acts
* accordingly, like reconnecting/relaunching language servers
*/
async function refreshSettings(event: vscode.ConfigurationChangeEvent, context: ExtensionContext) {
// Check if changes to language server paths were made
if (event.affectsConfiguration(`${constants.SETTINGS_NAMESPACE}.serverPath`)) {
for (const client of clients.values()) {
const folder = client.clientOptions.workspaceFolder;
await client_handler.stopClient(client);
if (folder) {
await startClientsForFolder(folder, context);
}
}
}
//
else {
for (const client of clients.values()) {
client.sendNotification("workspace/didChangeConfiguration", {
settings: {
...workspace.getConfiguration(constants.SETTINGS_NAMESPACE),
checkForUpdates: false,
},
});
}
}
}

154
client/src/server_binary.ts Normal file
View File

@ -0,0 +1,154 @@
// Local Imports
import constants = require('./constants');
import { outputChannel } from './extension';
// External Imports
import * as path from 'path';
import { stat } from "fs/promises";
import * as vscode from "vscode";
/**
* Tries to find a language server executable in the following places:
* - $PATH or $HOME directory
* - The open project's VSCode workspace settings
* - Near the unicorns (joking, this one isn't real, just like the unicorns)
*/
export async function findLanguageServerPath(): Promise<string | undefined> {
let customPath = getCustomLanguageServerPath();
if (customPath !== undefined) { return customPath }
// TODO: Think if we should also prompt and do updates
if (promptInstallLanguageServerExecutable()) {
if (installLanguageServerExecutable()) {
return searchInstalledLanguageServer();
}
}
return undefined
}
/**
* Checks for a custom language server binary set in the Workspace or User settings
*/
function getCustomLanguageServerPath(): string | undefined {
const workspaceConfig = vscode.workspace.getConfiguration(constants.EXTENSION_NAME);
// Check for custom language server path
if (workspaceConfig.get("serverBinaryPath") && vscode.workspace.workspaceFolders !== undefined) {
outputChannel.appendLine(
path.resolve(
vscode.workspace.workspaceFolders[0].uri.fsPath,
workspaceConfig.get("serverBinaryPath") as string
)
);
return path.resolve(
vscode.workspace.workspaceFolders[0].uri.fsPath,
workspaceConfig.get("serverBinaryPath") as string
);
}
}
/**
* Prompt the user about installing the language server from a known origin
* and installs it if agreed
*/
async function promptInstallLanguageServerExecutable(): Promise<boolean> {
const choice = await vscode.window.showWarningMessage(
"Could not find a language server binary.\
Would you like to install one using `cargo install`?",
{},
"Yes"
);
// TODO: The prompt could also say what places we tried to look into
// and consequentially a better message on from where we are getting it
// and to where it is being installed
if (!choice) {
return false;
}
}
/**
* Download language server executable from mirrors defined in `package.json`
* and put them
* @returns
*/
async function installLanguageServerExecutable(): Promise<boolean> {
return false
}
/**
* Looks in known paths like HOME(Linux, Windows, Unix) and UserProfile(Windows)
*/
function searchInstalledLanguageServer() {
let base = process.env["HOME"];
if (process.platform === "win32") {
base = process.env["USERPROFILE"];
}
if (!base) {
return undefined
}
return undefined;
}
/**
* Looks for the cargo binary path in HOME
* @returns Path of the executables directory for cargo
*/
function lookForCargoBinPath() {
let base = process.env["HOME"];
if (!base) { return undefined}
const cargoInstallRoot = process.env["CARGO_INSTALL_ROOT"];
if (cargoInstallRoot) {
return cargoInstallRoot;
}
const cargoHome = process.env["CARGO_HOME"];
if (cargoHome) {
return path.join(cargoHome, "bin");
}
return path.join(base, ".cargo", "bin");
}
/**
* Gives language server binary name based on current environment
*/
function getBinaryName(): string {
switch (process.platform) {
case "win32":
return "openquest-ls.exe"
case "linux":
return "openquest-ls"
default:
return "openquest-ls"
}
}
/** Checks if a given path exists */
async function checkPathValidity(path: string): Promise<boolean> {
try {
await stat(path)
return true
} catch (_) {
return false
}
}

View File

View File

@ -0,0 +1 @@
ANY browsers, ANY OS.

View File

@ -0,0 +1,7 @@
{
// "openquest.handlingMode": "Remote/Local Connection",
// "openquest.serverAddress": "localhost:4000"
"openquest.handlingMode": "Local Path",
"openquest.serverBinaryPath": "/run/media/nibunta/OMTRON/Projectos/Metin_Projects/General Projects/openquest/openquest-rs/target/debug/openquest-lsp",
"openquest.serverArguments": ["--transport", "stdio"]
}

View File

@ -0,0 +1,5 @@
local thing = 10
function test()
end

View File

@ -0,0 +1,7 @@
define thing 10
define some 100
quest some with thing begin
state start begin
end
end

View File

@ -0,0 +1 @@
#10

13
client/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"lib": ["es2020"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

View File

@ -0,0 +1,30 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "--",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": [ "--[[", "]]--" ]
},
// symbols used as brackets
"brackets": [
["begin", "end"],
["[", "]"],
["(", ")"]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
["begin", "end"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
// symbols that can be used to surround a selection
"surroundingPairs": [
["begin", "end"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
]
}

5236
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

103
package.json Normal file
View File

@ -0,0 +1,103 @@
{
"name": "openquest",
"displayName": "OpenQuest",
"description": "Support for the Quest language, a DSL for mission scripting (Metin2)",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "https://github.com/OriDevTeam/openquest-rs\""
},
"license": "Apache2",
"publisher": "OriDevTeam",
"categories": [
"Programming Languages"
],
"keywords": [
"quest",
"language"
],
"engines": {
"vscode": "^1.78.0"
},
"main": "./client/out/extension",
"contributes": {
"languages": [
{
"id": "quest",
"aliases": [
"Quest",
"quest"
],
"extensions": [
".quest"
],
"configuration": "./language-configuration.json",
"embeddedLanguages": { "meta.embedded.block.lua": "lua" }
}
],
"grammars": [
{
"language": "quest",
"scopeName": "source.quest",
"path": "./syntaxes/quest.tmLanguage.json"
}
],
"configuration": {
"title": "OpenQuest",
"properties": {
"openquest.handlingMode": {
"scope": "window",
"type": "string",
"enum": [
"Automatic (Package Manager)",
"Local Path",
"Remote/Local Connection"
],
"default": "Automatic (Package Manager)",
"description": "Where and how to connect to a language server, or spawn a new process of one"
},
"openquest.serverAddress": {
"scope": "window",
"type": "string",
"markdownDescription": "Language Server address to connect to (**Example:** localhost:5050)"
},
"openquest.serverArguments": {
"scope": "window",
"type": "array",
"default": [],
"description": "Arguments to pass when initializing language server"
},
"openquest.serverBinaryPath": {
"scope": "window",
"type": "string",
"markdownDescription": "Path of the language server executable (if not given, it will look in $PATH and consequentially $HOME)"
}
}
}
},
"scripts": {
"vscode:prepublish": "pnpm run compile",
"compile": "tsc -b",
"postinstall": "cd client && pnpm install && cd ..",
"watch": "tsc -b -w",
"lint": "eslint ./client/src ./server/src --ext .ts,.tsx",
"test": "sh ./scripts/e2e.sh",
"package-y2j": "js-yaml package.yaml > package.json",
"grammar-y2j": "js-yaml syntaxes/quest.tmGrammar.yaml > syntaxes/quest.tmGrammar.json"
},
"dependencies": {
"generator-code": "^1.7.7",
"js-yaml": "^4.1.0",
"ws": "^8.13.0"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"@types/node": "^16.18.34",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"@types/ws": "^8.5.5",
"eslint": "^8.42.0",
"mocha": "^9.2.2",
"typescript": "^5.1.3"
}
}

80
packagee.yaml Normal file
View File

@ -0,0 +1,80 @@
name: openquest
displayName: OpenQuest
description: Support for the Quest language, a DSL for mission scripting (Metin2)
version: 0.0.1
repository:
type: git
url: https://github.com/OriDevTeam/openquest-rs"
license: Apache2
publisher: OriDevTeam
categories:
- Programming Languages
keywords:
- quest
- language
engines: {vscode: ^1.78.0}
main: ./client/out/extension
contributes:
languages:
- id: quest
aliases:
- Quest
- quest
extensions:
- .quest
configuration: ./language-configuration.json
grammars:
- language: quest
scopeName: source.quest
path: ./syntaxes/quest.tmLanguage.json
configuration:
title: OpenQuest
properties:
openquest.handlingMode:
scope: window
type: string
enum:
- Local Path
- WebSocket
default: Local Path
description: >-
Where and how to connect to a language server, or spawn a new process
of one
openquest.serverAddress:
scope: window
type: string
markdownDescription: Language Server address to connect to (**Example:** localhost:5050)
openquest.serverArguments:
scope: window
type: string
description: Arguments to pass when initializing language server
openquest.serverBinaryPath:
scope: window
type: string
markdownDescription: >-
Path of the language server executable (if not given, it will look in
$PATH and consequentially $HOME)
scripts:
vscode:prepublish: pnpm run compile
compile: tsc -b
postinstall: cd client && pnpm install && cd ..
watch: tsc -b -w
lint: eslint ./client/src ./server/src --ext .ts,.tsx
test: sh ./scripts/e2e.sh
package-y2j: js-yaml package.yaml > package.json
grammar-y2j: js-yaml syntaxes/quest.tmGrammar.yaml > syntaxes/quest.tmGrammar.json
dependencies:
generator-code: ^1.7.7
js-yaml: ^4.1.0
ws: ^8.13.0
devDependencies:
'@types/mocha': ^9.1.1
'@types/node': ^16.18.34
'@typescript-eslint/eslint-plugin': ^5.59.9
'@typescript-eslint/parser': ^5.59.9
'@types/ws': ^8.5.5
eslint: ^8.42.0
mocha: ^9.2.2
typescript: ^5.1.3

View File

@ -0,0 +1,33 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"scopeName": "source.quest",
"patterns": [
{
"include": "#quest_clause"
}
],
"repository": {
"quest": {
"patterns": [
{
"include": "#define_substitution"
},
{
"include": "#quest_clause"
}
]
},
"define_substitution": {
"name": "keyword.local.define_substitution"
},
"quest_clause": {
"name": "keyword.control.quest_clause"
},
"state_clause": {
"name": "keyword.control.state_clause"
},
"when_clause": {
"name": "keyword.control.when_clause"
}
}
}

View File

@ -0,0 +1,31 @@
$schema: https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json
scopeName: source.quest
patterns:
- {include: "#quest_clause"}
repository:
quest:
patterns:
- {include: "#define_substitution"}
- {include: "#quest_clause"}
# Pattern syntax:
# define <name> <value(string, decimal, float, any)>
define_substitution:
name: keyword.local.define_substitution
# Pattern syntax:
# quest <name> begin <define* | state_clause+> end
quest_clause:
name: keyword.control.quest_clause
# Pattern syntax:
# state <name> begin <with_clause+> end
state_clause:
name: keyword.control.state_clause
# Pattern syntax:
# when <name | name.cond> with <lua_expression> begin <lua_expression> end
when_clause:
name: keyword.control.when_clause

View File

@ -0,0 +1,59 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Quest",
"scopeName": "source.quest",
"patterns": [
{"include": "#define_substitution"},
{"include": "#quest_clause"}
],
"repository": {
"define_substitution": {
"name": "keyword.control.quest",
"match": "\\b(define)\\s(.+)\\s(.+)\\b",
"captures": {
"1": {"name": "keyword.function"},
"2": {"name": "entity.other.attribute-name"},
"3": {"name": "variable.other"}
}
},
"quest_clause": {
"begin": "\\b(quest)\\s(.+)\\s(with)\\s(.+)\\s(begin)",
"beginCaptures": {
"1": {"name": "keyword"},
"2": {"name": "entity.other.attribute-name"},
"3": {"name": "keyword"},
"4": {"name": "variable.other"},
"5": {"name": "punctuation.paren.open"}
},
"end": "end", "endCaptures": {"0": {"name": "punctuation.paren.close"}},
"patterns": [{"include": "#state_clause"}]
},
"state_clause": {
"begin": "(state)\\s(.+)\\s(begin)",
"beginCaptures": {
"1": {"name": "keyword"},
"2": {"name": "entity.other.attribute-name"},
"3": {"name": "punctuation.paren.open"}
},
"end": "end", "endCaptures": {"0": {"name": "punctuation.paren.close"}},
"patterns": [{"include": "#with_clause"}]
},
"with_clause": {
"name": "keyword.control.with_clause",
"begin": "when\\s(.+)\\sbegin",
"beginCaptures": {
"0": {"name": "punctuation.paren.open"},
"1": {"name": "storage.modifier.quest"}
},
"end": "end", "endCaptures": {"0": {"name": "punctuation.paren.close"}}
},
"lua_scope": {
}
}
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"lib": ["es2020"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
".vscode-test"
],
"references": [
{ "path": "./client" },
]
}

View File

@ -0,0 +1,29 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension.
* `syntaxes/quest.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization.
* `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets.
## Get up and running straight away
* Make sure the language configuration settings in `language-configuration.json` are accurate.
* Press `F5` to open a new window with your extension loaded.
* Create a new file with a file name suffix matching your language.
* Verify that syntax highlighting works and that the language configuration settings are working.
## Make changes
* You can relaunch the extension from the debug toolbar after making changes to the files listed above.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Add more language features
* To add features such as IntelliSense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs
## Install your extension
* To start using your extension with Visual Studio Code copy it into the `<user home>/.vscode/extensions` folder and restart Code.
* To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension.