Format (#167)
This commit is contained in:
parent
c93201a909
commit
94038d3297
96 changed files with 2777 additions and 3003 deletions
|
@ -6,8 +6,5 @@
|
|||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"@example/*",
|
||||
"www"
|
||||
]
|
||||
"ignore": ["@example/*", "www"]
|
||||
}
|
||||
|
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
**/dist/*
|
62
.vscode/launch.json
vendored
62
.vscode/launch.json
vendored
|
@ -1,36 +1,32 @@
|
|||
// 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}/vscode"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/vscode/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": {
|
||||
"type": "npm",
|
||||
"script": "build:extension"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Server",
|
||||
"port": 6040,
|
||||
"restart": true,
|
||||
"outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"]
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Launch Extension",
|
||||
"configurations": ["Launch Client", "Attach to Server"]
|
||||
}
|
||||
]
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"name": "Launch Client",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}/vscode"],
|
||||
"outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"],
|
||||
"preLaunchTask": {
|
||||
"type": "npm",
|
||||
"script": "build:extension"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Server",
|
||||
"port": 6040,
|
||||
"restart": true,
|
||||
"outFiles": ["${workspaceRoot}/vscode/dist/**/*.js"]
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Launch Extension",
|
||||
"configurations": ["Launch Client", "Attach to Server"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
29
.vscode/tasks.json
vendored
29
.vscode/tasks.json
vendored
|
@ -1,18 +1,15 @@
|
|||
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:extension",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
}
|
||||
]
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:extension",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": ["$tsc"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ npm run build
|
|||
|
||||
To deploy your Astro site to production, upload the contents of `/dist` to your favorite static site host.
|
||||
|
||||
|
||||
## 🥾 Guides
|
||||
|
||||
### 🚀 Basic Usage
|
||||
|
@ -66,8 +65,8 @@ Even though nearly-everything [is configurable][docs-config], we recommend start
|
|||
|
||||
Routing happens in `src/pages/*`. Every `.astro` or `.md.astro` file in this folder corresponds with a public URL. For example:
|
||||
|
||||
| Local file | Public URL |
|
||||
| :--------------------------------------- | :------------------------------ |
|
||||
| Local file | Public URL |
|
||||
| :------------------------------------- | :------------------------------ |
|
||||
| `src/pages/index.astro` | `/index.html` |
|
||||
| `src/pages/post/my-blog-post.md.astro` | `/post/my-blog-post/index.html` |
|
||||
|
||||
|
@ -164,10 +163,10 @@ Astro will automatically create a `/sitemap.xml` for you for SEO! Be sure to set
|
|||
|
||||
👉 [**Collections API**][docs-collections]
|
||||
|
||||
|
||||
## ⚙️ Config
|
||||
|
||||
👉 [**`astro.config.mjs` Reference**][docs-config]
|
||||
|
||||
## 📚 API
|
||||
|
||||
👉 [**Full API Reference**][docs-api]
|
||||
|
|
|
@ -36,7 +36,7 @@ Runs the Astro development server. This starts an HTTP server that responds to r
|
|||
|
||||
See the [dev server](./dev.md) docs for more information on how the dev server works.
|
||||
|
||||
__Flags__
|
||||
**Flags**
|
||||
|
||||
##### `--port`
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ The dev server will serve the following special routes:
|
|||
|
||||
### /400
|
||||
|
||||
This is a custom __400__ status code page. You can add this route by adding a page component to your `src/pages` folder:
|
||||
This is a custom **400** status code page. You can add this route by adding a page component to your `src/pages` folder:
|
||||
|
||||
```
|
||||
├── src/
|
||||
|
@ -27,13 +27,10 @@ For any URL you visit that doesn't have a corresponding page, the `400.astro` fi
|
|||
|
||||
### /500
|
||||
|
||||
This is a custom __500__ status code page. You can add this route by adding a page component to your `src/pages` folder:
|
||||
This is a custom **500** status code page. You can add this route by adding a page component to your `src/pages` folder:
|
||||
|
||||
```astro
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ └── pages/
|
||||
│ └── 500.astro
|
||||
├── src/ │ ├── components/ │ └── pages/ │ └── 500.astro
|
||||
```
|
||||
|
||||
This page is used any time an error occurs in the dev server.
|
||||
|
|
|
@ -116,26 +116,25 @@ export let name;
|
|||
|
||||
`.astro` files can end up looking very similar to `.jsx` files, but there are a few key differences. Here's a comparison between the two formats.
|
||||
|
||||
| Feature | Astro | JSX |
|
||||
|------------------------- |------------------------------------------ |---------------------------------------------------- |
|
||||
| File extension | `.astro` | `.jsx` or `.tsx` |
|
||||
| User-Defined Components | `<Capitalized>` | `<Capitalized>` |
|
||||
| Expression Syntax | `{}` | `{}` |
|
||||
| Spread Attributes | `{...props}` | `{...props}` |
|
||||
| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` |
|
||||
| Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` |
|
||||
| IDE Support | WIP - [VS Code][code-ext] | Phenomenal |
|
||||
| Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope |
|
||||
| Fragments | Automatic | Wrap with `<Fragment>` or `<>` |
|
||||
| Multiple frameworks per-file | Yes | No |
|
||||
| Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) |
|
||||
| Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` |
|
||||
| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
|
||||
| Attributes | `dash-case` | `camelCase` |
|
||||
| Feature | Astro | JSX |
|
||||
| ---------------------------- | ---------------------------------------- | -------------------------------------------------- |
|
||||
| File extension | `.astro` | `.jsx` or `.tsx` |
|
||||
| User-Defined Components | `<Capitalized>` | `<Capitalized>` |
|
||||
| Expression Syntax | `{}` | `{}` |
|
||||
| Spread Attributes | `{...props}` | `{...props}` |
|
||||
| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` |
|
||||
| Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` |
|
||||
| IDE Support | WIP - [VS Code][code-ext] | Phenomenal |
|
||||
| Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope |
|
||||
| Fragments | Automatic | Wrap with `<Fragment>` or `<>` |
|
||||
| Multiple frameworks per-file | Yes | No |
|
||||
| Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) |
|
||||
| Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` |
|
||||
| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
|
||||
| Attributes | `dash-case` | `camelCase` |
|
||||
|
||||
### TODO: Styling
|
||||
|
||||
### TODO: Composition (Slots)
|
||||
|
||||
|
||||
[code-ext]: https://marketplace.visualstudio.com/items?itemName=astro-build.astro
|
||||
|
|
|
@ -3,18 +3,18 @@ import { useState } from 'preact/hooks';
|
|||
|
||||
/** a counter written in Preact */
|
||||
export default function PreactCounter({ children }) {
|
||||
const [count, setCount] = useState(0)
|
||||
const add = () => setCount(i => i + 1);
|
||||
const subtract = () => setCount(i => i - 1);
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
||||
return <>
|
||||
<div className="counter">
|
||||
return (
|
||||
<>
|
||||
<div className="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
<div className="children">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
<div className="children">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,18 +2,18 @@ import React, { useState } from 'react';
|
|||
|
||||
/** a counter written in React */
|
||||
export default function ReactCounter({ children }) {
|
||||
const [count, setCount] = useState(0)
|
||||
const add = () => setCount(i => i + 1);
|
||||
const subtract = () => setCount(i => i - 1);
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
||||
return <>
|
||||
<div className="counter">
|
||||
return (
|
||||
<>
|
||||
<div className="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
<div className="children">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
<div className="children">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
# Astro Demo
|
||||
|
||||
## Getting setup
|
||||
## Getting set up
|
||||
|
||||
1. Checkout Astro at: https://github.com/snowpackjs/astro
|
||||
|
||||
1. Install and build Astro:
|
||||
1. Check out Astro at: https://github.com/snowpackjs/astro
|
||||
|
||||
```shell
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
1. Install and build Astro:
|
||||
|
||||
2. Link Astro:
|
||||
```shell
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
```shell
|
||||
npm link
|
||||
```
|
||||
2. Link Astro:
|
||||
|
||||
```shell
|
||||
npm link
|
||||
```
|
||||
|
||||
2. In this project link Astro and install other deps:
|
||||
|
||||
|
@ -34,4 +34,4 @@ npm run start
|
|||
|
||||
```shell
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {h} from 'preact';
|
||||
import { h } from 'preact';
|
||||
|
||||
export default function CompanyLogo({ user }) {
|
||||
return (
|
||||
|
|
|
@ -117,5 +117,9 @@ function PluginSearchPageLive() {
|
|||
}
|
||||
|
||||
export default function PluginSearchPage(props) {
|
||||
return import.meta.env.astro ? <div>Loading...</div> : <PluginSearchPageLive {...props} />
|
||||
return import.meta.env.astro ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<PluginSearchPageLive {...props} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import docsearch from 'docsearch.js/dist/cdn/docsearch.min.js';
|
||||
|
||||
customElements.define('doc-search', class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if(!this._setup) {
|
||||
const apiKey = this.getAttribute('api-key');
|
||||
const selector = this.getAttribute('selector');
|
||||
docsearch({
|
||||
apiKey: apiKey,
|
||||
indexName: 'snowpack',
|
||||
inputSelector: selector,
|
||||
debug: true // Set debug to true if you want to inspect the dropdown
|
||||
});
|
||||
this._setup = true;
|
||||
customElements.define(
|
||||
'doc-search',
|
||||
class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
if (!this._setup) {
|
||||
const apiKey = this.getAttribute('api-key');
|
||||
const selector = this.getAttribute('selector');
|
||||
docsearch({
|
||||
apiKey: apiKey,
|
||||
indexName: 'snowpack',
|
||||
inputSelector: selector,
|
||||
debug: true, // Set debug to true if you want to inspect the dropdown
|
||||
});
|
||||
this._setup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
const snowpackManifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../../snowpack/package.json'), 'utf8'));
|
||||
const snowpackManifest = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../../snowpack/package.json'), 'utf8'),
|
||||
);
|
||||
export default snowpackManifest.version;
|
||||
|
|
|
@ -28,4 +28,4 @@ const worker = new Worker(new URL('./esm-worker.js', import.meta.url), {
|
|||
name: 'my-worker',
|
||||
type: 'module',
|
||||
});
|
||||
```
|
||||
```
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"ignoreChanges": [
|
||||
"**/test/**",
|
||||
"**/*.md"
|
||||
],
|
||||
"ignoreChanges": ["**/test/**", "**/*.md"],
|
||||
"useWorkspaces": true,
|
||||
"version": "4.0.0"
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
"release": "yarn build && yarn changeset publish",
|
||||
"build": "yarn build:core",
|
||||
"build:core": "lerna run build --scope astro --scope astro-parser --scope create-astro",
|
||||
"format": "prettier -w '**/*.{js,jsx,ts,tsx,md,json}'",
|
||||
"lint": "eslint 'packages/**/*.ts'",
|
||||
"test": "yarn test:core && yarn test:prettier",
|
||||
"test:core": "cd packages/astro && npm test",
|
||||
"test:prettier": "cd tools/prettier-plugin-astro && npm test",
|
||||
"format": "prettier -w '**/*.{js,jsx,ts,tsx,md,json}'"
|
||||
"test:prettier": "cd tools/prettier-plugin-astro && npm test"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
|
18
packages/astro/internal.d.ts
vendored
18
packages/astro/internal.d.ts
vendored
|
@ -1,27 +1,27 @@
|
|||
declare module '#astro/compiler' {
|
||||
export * from 'astro/dist/types/compiler';
|
||||
export * from 'astro/dist/types/compiler';
|
||||
}
|
||||
declare module '#astro/ast' {
|
||||
export * from 'astro/dist/types/ast';
|
||||
export * from 'astro/dist/types/ast';
|
||||
}
|
||||
declare module '#astro/build' {
|
||||
export * from 'astro/dist/types/build';
|
||||
export * from 'astro/dist/types/build';
|
||||
}
|
||||
declare module '#astro/cli' {
|
||||
export * from 'astro/dist/types/cli';
|
||||
export * from 'astro/dist/types/cli';
|
||||
}
|
||||
declare module '#astro/config' {
|
||||
export * from 'astro/dist/types/config';
|
||||
export * from 'astro/dist/types/config';
|
||||
}
|
||||
declare module '#astro/dev' {
|
||||
export * from 'astro/dist/types/dev';
|
||||
export * from 'astro/dist/types/dev';
|
||||
}
|
||||
declare module '#astro/logger' {
|
||||
export * from 'astro/dist/types/logger';
|
||||
export * from 'astro/dist/types/logger';
|
||||
}
|
||||
declare module '#astro/runtime' {
|
||||
export * from 'astro/dist/types/runtime';
|
||||
export * from 'astro/dist/types/runtime';
|
||||
}
|
||||
declare module '#astro/search' {
|
||||
export * from 'astro/dist/types/search';
|
||||
export * from 'astro/dist/types/search';
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import { generateRSS } from './build/rss.js';
|
|||
import { generateSitemap } from './build/sitemap.js';
|
||||
import { collectStatics } from './build/static.js';
|
||||
import { canonicalURL } from './build/util.js';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const { mkdir, readFile, writeFile } = fsPromises;
|
||||
|
||||
|
@ -69,8 +68,8 @@ async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf8'
|
|||
interface WriteResultOptions {
|
||||
srcPath: string;
|
||||
result: LoadResult;
|
||||
outPath: URL,
|
||||
encoding: null|'utf8'
|
||||
outPath: URL;
|
||||
encoding: null | 'utf8';
|
||||
}
|
||||
|
||||
/** Utility for writing a build result to disk */
|
||||
|
|
|
@ -31,10 +31,10 @@ interface CLIState {
|
|||
/** Determine which action the user requested */
|
||||
function resolveArgs(flags: Arguments): CLIState {
|
||||
const options: CLIState['options'] = {
|
||||
projectRoot: typeof flags.projectRoot === 'string' ? flags.projectRoot: undefined,
|
||||
projectRoot: typeof flags.projectRoot === 'string' ? flags.projectRoot : undefined,
|
||||
sitemap: typeof flags.sitemap === 'boolean' ? flags.sitemap : undefined,
|
||||
port: typeof flags.port === 'number' ? flags.port : undefined,
|
||||
config: typeof flags.config === 'string' ? flags.config : undefined
|
||||
config: typeof flags.config === 'string' ? flags.config : undefined,
|
||||
};
|
||||
|
||||
if (flags.version) {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
|
|||
import { transform } from './transform/index.js';
|
||||
import { codegen } from './codegen/index.js';
|
||||
|
||||
export { scopeRule } from './transform/postcss-scoped-styles/index.js'
|
||||
export { scopeRule } from './transform/postcss-scoped-styles/index.js';
|
||||
|
||||
/** Return Astro internal import URL */
|
||||
function internalImport(internalPath: string) {
|
||||
|
|
|
@ -26,8 +26,8 @@ function validateConfig(config: any): void {
|
|||
}
|
||||
}
|
||||
|
||||
if(typeof config.devOptions?.port !== 'number') {
|
||||
throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`)
|
||||
if (typeof config.devOptions?.port !== 'number') {
|
||||
throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ export default async function dev(astroConfig: AstroConfig) {
|
|||
res.statusCode = 500;
|
||||
|
||||
let errorResult = await runtime.load(`/500?error=${encodeURIComponent(result.error.stack || result.error.toString())}`);
|
||||
if(errorResult.statusCode === 200) {
|
||||
if (errorResult.statusCode === 200) {
|
||||
if (errorResult.contentType) {
|
||||
res.setHeader('Content-Type', errorResult.contentType);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,6 @@ export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRe
|
|||
};
|
||||
return tree.map((subtree) => {
|
||||
if (subtree.type === 'text') return JSON.stringify(subtree.value);
|
||||
return toH(innerH, subtree).__SERIALIZED
|
||||
return toH(innerH, subtree).__SERIALIZED;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,13 +16,13 @@ export const defaultLogDestination = new Writable({
|
|||
dest = process.stdout;
|
||||
}
|
||||
let type = event.type;
|
||||
if(type !== null) {
|
||||
if (type !== null) {
|
||||
if (event.level === 'info') {
|
||||
type = bold(blue(type));
|
||||
} else if (event.level === 'error') {
|
||||
type = bold(red(type));
|
||||
}
|
||||
|
||||
|
||||
dest.write(`[${type}] `);
|
||||
}
|
||||
|
||||
|
@ -135,10 +135,10 @@ export const logger = {
|
|||
};
|
||||
|
||||
// For silencing libraries that go directly to console.warn
|
||||
export function trapWarn(cb: (...args: any[]) => void = () =>{}) {
|
||||
export function trapWarn(cb: (...args: any[]) => void = () => {}) {
|
||||
const warn = console.warn;
|
||||
console.warn = function(...args: any[]) {
|
||||
console.warn = function (...args: any[]) {
|
||||
cb(...args);
|
||||
};
|
||||
return () => console.warn = warn;
|
||||
return () => (console.warn = warn);
|
||||
}
|
||||
|
|
|
@ -200,14 +200,14 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
|
||||
// For first release query params are not passed to components.
|
||||
// An exception is made for dev server specific routes.
|
||||
if(reqPath !== '/500') {
|
||||
if (reqPath !== '/500') {
|
||||
requestURL.search = '';
|
||||
}
|
||||
|
||||
let html = (await mod.exports.__renderPage({
|
||||
request: {
|
||||
// params should go here when implemented
|
||||
url: requestURL
|
||||
url: requestURL,
|
||||
},
|
||||
children: [],
|
||||
props: { collection },
|
||||
|
|
|
@ -92,14 +92,14 @@ export function searchForPage(url: URL, astroRoot: URL): SearchResult {
|
|||
}
|
||||
}
|
||||
|
||||
if(reqPath === '/500') {
|
||||
if (reqPath === '/500') {
|
||||
return {
|
||||
statusCode: 200,
|
||||
location: {
|
||||
fileURL: new URL('./frontend/500.astro', import.meta.url),
|
||||
snowpackURL: `/_astro_internal/500.astro.js`
|
||||
snowpackURL: `/_astro_internal/500.astro.js`,
|
||||
},
|
||||
pathname: reqPath
|
||||
pathname: reqPath,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import * as assert from 'uvu/assert';
|
|||
import { loadConfig } from '#astro/config';
|
||||
import { createRuntime } from '#astro/runtime';
|
||||
|
||||
|
||||
const DType = suite('doctype');
|
||||
|
||||
let runtime, setupError;
|
||||
|
|
|
@ -12,7 +12,7 @@ ConfigPath('can be passed via --config', async (context) => {
|
|||
|
||||
process.stdout.setEncoding('utf8');
|
||||
for await (const chunk of process.stdout) {
|
||||
if(/Server started/.test(chunk)) {
|
||||
if (/Server started/.test(chunk)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ ConfigPort('can be specified via --port flag', async (context) => {
|
|||
|
||||
process.stdout.setEncoding('utf8');
|
||||
for await (const chunk of process.stdout) {
|
||||
if(/Local:/.test(chunk)) {
|
||||
if (/Local:/.test(chunk)) {
|
||||
assert.ok(/:3002/.test(chunk), 'Using the right port');
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -3,4 +3,4 @@ layout: ../../layouts/base.astro
|
|||
title: My Page
|
||||
---
|
||||
|
||||
Hello world
|
||||
Hello world
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function PreactComponent({ children }) {
|
||||
return <div id="preact">{children}</div>
|
||||
return <div id="preact">{children}</div>;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function() {
|
||||
export default function () {
|
||||
return (
|
||||
<div>
|
||||
<button type="button">Increment -</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function({ name }) {
|
||||
return <div id={name}>{name}</div>
|
||||
}
|
||||
export default function ({ name }) {
|
||||
return <div id={name}>{name}</div>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function(props) {
|
||||
return (
|
||||
<div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div>
|
||||
);
|
||||
};
|
||||
export default function (props) {
|
||||
return <div id="fallback">{import.meta.env.astro ? 'static' : 'dynamic'}</div>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function() {
|
||||
return <div id="test">Testing</div>
|
||||
}
|
||||
export default function () {
|
||||
return <div id="test">Testing</div>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export default function({ name }) {
|
||||
return <div id="test">Hello {name}</div>
|
||||
}
|
||||
export default function ({ name }) {
|
||||
return <div id="test">Hello {name}</div>;
|
||||
}
|
||||
|
|
|
@ -10,4 +10,4 @@ import:
|
|||
## Interesting Topic
|
||||
|
||||
<Hello name={`world`} />
|
||||
<Counter:load />
|
||||
<Counter:load />
|
||||
|
|
|
@ -4,4 +4,3 @@ import cheerio from 'cheerio';
|
|||
export function doc(html) {
|
||||
return cheerio.load(html);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# create-astro
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed63132: Added **interactive mode** with a fesh new UI.
|
||||
|
||||
|
||||
Included a new **blank** starter to get up and running even faster.
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
# create-astro
|
||||
|
||||
## Scaffolding for Astro projects
|
||||
|
||||
**With NPM:**
|
||||
|
||||
```bash
|
||||
npm init astro
|
||||
```
|
||||
|
||||
**With Yarn:**
|
||||
|
||||
```bash
|
||||
yarn create astro
|
||||
```
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useEffect} from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { prepareTemplate, isEmpty, emptyDir } from '../utils';
|
||||
import Header from './Header';
|
||||
import Install from './Install';
|
||||
|
@ -8,86 +8,91 @@ import Confirm from './Confirm';
|
|||
import Finalize from './Finalize';
|
||||
|
||||
interface Context {
|
||||
use: 'npm'|'yarn';
|
||||
run: boolean;
|
||||
projectExists?: boolean;
|
||||
force?: boolean;
|
||||
projectName?: string;
|
||||
template?: string;
|
||||
templates: string[];
|
||||
ready?: boolean;
|
||||
use: 'npm' | 'yarn';
|
||||
run: boolean;
|
||||
projectExists?: boolean;
|
||||
force?: boolean;
|
||||
projectName?: string;
|
||||
template?: string;
|
||||
templates: string[];
|
||||
ready?: boolean;
|
||||
}
|
||||
|
||||
const getStep = ({ projectName, projectExists: exists, template, force, ready }: Context) => {
|
||||
switch (true) {
|
||||
case !projectName: return {
|
||||
key: 'projectName',
|
||||
Component: ProjectName
|
||||
};
|
||||
case projectName && exists === true && typeof force === 'undefined': return {
|
||||
key: 'force',
|
||||
Component: Confirm
|
||||
}
|
||||
case (exists === false || force) && !template: return {
|
||||
key: 'template',
|
||||
Component: Template
|
||||
};
|
||||
case !ready: return {
|
||||
key: 'install',
|
||||
Component: Install
|
||||
};
|
||||
default: return {
|
||||
key: 'final',
|
||||
Component: Finalize
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (true) {
|
||||
case !projectName:
|
||||
return {
|
||||
key: 'projectName',
|
||||
Component: ProjectName,
|
||||
};
|
||||
case projectName && exists === true && typeof force === 'undefined':
|
||||
return {
|
||||
key: 'force',
|
||||
Component: Confirm,
|
||||
};
|
||||
case (exists === false || force) && !template:
|
||||
return {
|
||||
key: 'template',
|
||||
Component: Template,
|
||||
};
|
||||
case !ready:
|
||||
return {
|
||||
key: 'install',
|
||||
Component: Install,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
key: 'final',
|
||||
Component: Finalize,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const App: FC<{ context: Context }> = ({ context }) => {
|
||||
const [state, setState] = React.useState(context);
|
||||
const step = React.useRef(getStep(context));
|
||||
const onSubmit = (value: string|boolean) => {
|
||||
const { key } = step.current;
|
||||
const newState = { ...state, [key]: value };
|
||||
step.current = getStep(newState)
|
||||
setState(newState)
|
||||
}
|
||||
const [state, setState] = React.useState(context);
|
||||
const step = React.useRef(getStep(context));
|
||||
const onSubmit = (value: string | boolean) => {
|
||||
const { key } = step.current;
|
||||
const newState = { ...state, [key]: value };
|
||||
step.current = getStep(newState);
|
||||
setState(newState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true
|
||||
if (state.projectName && typeof state.projectExists === 'undefined') {
|
||||
const newState = { ...state, projectExists: !isEmpty(state.projectName) };
|
||||
step.current = getStep(newState)
|
||||
if (isSubscribed) {
|
||||
setState(newState);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
if (state.projectName && typeof state.projectExists === 'undefined') {
|
||||
const newState = { ...state, projectExists: !isEmpty(state.projectName) };
|
||||
step.current = getStep(newState);
|
||||
if (isSubscribed) {
|
||||
setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.projectName && (state.projectExists === false || state.force) && state.template) {
|
||||
if (state.force) emptyDir(state.projectName);
|
||||
prepareTemplate(context.use, state.template, state.projectName).then(() => {
|
||||
if (isSubscribed) {
|
||||
setState(v => {
|
||||
const newState = {...v, ready: true };
|
||||
step.current = getStep(newState);
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (state.projectName && (state.projectExists === false || state.force) && state.template) {
|
||||
if (state.force) emptyDir(state.projectName);
|
||||
prepareTemplate(context.use, state.template, state.projectName).then(() => {
|
||||
if (isSubscribed) {
|
||||
setState((v) => {
|
||||
const newState = { ...v, ready: true };
|
||||
step.current = getStep(newState);
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
}
|
||||
}, [state]);
|
||||
const { Component } = step.current;
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
};
|
||||
}, [state]);
|
||||
const { Component } = step.current;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header context={state}/>
|
||||
<Component context={state} onSubmit={onSubmit} />
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Header context={state} />
|
||||
<Component context={state} onSubmit={onSubmit} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -31,7 +31,7 @@ const Confirm: FC<{ message?: any; context: any; onSubmit: (value: boolean) => v
|
|||
items={[
|
||||
{
|
||||
value: false,
|
||||
label: 'no'
|
||||
label: 'no',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
|
|
|
@ -2,4 +2,4 @@ import React from 'react';
|
|||
import { Text } from 'ink';
|
||||
import { isWin } from '../utils';
|
||||
|
||||
export default ({ children }) => isWin() ? null : <Text>{children}</Text>
|
||||
export default ({ children }) => (isWin() ? null : <Text>{children}</Text>);
|
||||
|
|
|
@ -2,8 +2,11 @@ import React, { FC } from 'react';
|
|||
import { Box, Text } from 'ink';
|
||||
import { isDone } from '../utils';
|
||||
|
||||
const Exit: FC<{ didError?: boolean }> = ({ didError }) => isDone ? null : <Box marginTop={1} display="flex">
|
||||
<Text color={didError ? "#FF1639" : "#FFBE2D"}>[abort]</Text>
|
||||
<Text> astro cancelled</Text>
|
||||
</Box>
|
||||
const Exit: FC<{ didError?: boolean }> = ({ didError }) =>
|
||||
isDone ? null : (
|
||||
<Box marginTop={1} display="flex">
|
||||
<Text color={didError ? '#FF1639' : '#FFBE2D'}>[abort]</Text>
|
||||
<Text> astro cancelled</Text>
|
||||
</Box>
|
||||
);
|
||||
export default Exit;
|
||||
|
|
|
@ -7,21 +7,26 @@ const Finalize: FC<{ context: any }> = ({ context: { use, projectName } }) => {
|
|||
cancelProcessListeners();
|
||||
process.exit(0);
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex">
|
||||
<Text color="#17C083">{'[ yes ]'}</Text>
|
||||
<Text> Project initialized at <Text color="#3894FF">./{projectName}</Text></Text>
|
||||
<Text>
|
||||
{' '}
|
||||
Project initialized at <Text color="#3894FF">./{projectName}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" marginY={1}>
|
||||
<Text dimColor>{'[ tip ]'}</Text>
|
||||
<Box display="flex" marginLeft={1} flexDirection="column">
|
||||
<Text>Get started by running</Text>
|
||||
<Text color="#3894FF">cd ./{projectName}</Text>
|
||||
<Text color="#3894FF">{use} start</Text>
|
||||
<Text>Get started by running</Text>
|
||||
<Text color="#3894FF">cd ./{projectName}</Text>
|
||||
<Text color="#3894FF">{use} start</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</>;
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Finalize;
|
||||
|
|
|
@ -2,19 +2,27 @@ import React from 'react';
|
|||
import { Box, Text } from 'ink';
|
||||
|
||||
const getMessage = ({ projectName, template }) => {
|
||||
switch (true) {
|
||||
case !projectName: return <Text dimColor>Gathering mission details</Text>;
|
||||
case !template: return <Text dimColor>Optimizing navigational system</Text>;
|
||||
default: return <Text color="black" backgroundColor="white"> {projectName} </Text>
|
||||
}
|
||||
}
|
||||
switch (true) {
|
||||
case !projectName:
|
||||
return <Text dimColor>Gathering mission details</Text>;
|
||||
case !template:
|
||||
return <Text dimColor>Optimizing navigational system</Text>;
|
||||
default:
|
||||
return (
|
||||
<Text color="black" backgroundColor="white">
|
||||
{' '}
|
||||
{projectName}{' '}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Header: React.FC<{ context: any }> = ({ context }) => (
|
||||
<Box width={48} display="flex" marginY={1}>
|
||||
<Text backgroundColor="#882DE7" color="white">{' astro '}</Text>
|
||||
<Box marginLeft={1}>
|
||||
{getMessage(context)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
<Box width={48} display="flex" marginY={1}>
|
||||
<Text backgroundColor="#882DE7" color="white">
|
||||
{' astro '}
|
||||
</Text>
|
||||
<Box marginLeft={1}>{getMessage(context)}</Box>
|
||||
</Box>
|
||||
);
|
||||
export default Header;
|
||||
|
|
|
@ -2,61 +2,79 @@ import React, { FC } from 'react';
|
|||
import { Box, Text } from 'ink';
|
||||
import { ARGS, ARG } from '../config';
|
||||
|
||||
const Type: FC<{ type: any, enum?: string[] }> = ({ type, enum: e }) => {
|
||||
if (type === Boolean) {
|
||||
return <>
|
||||
<Text color="#3894FF">true</Text>
|
||||
<Text dimColor>|</Text>
|
||||
<Text color="#3894FF">false</Text>
|
||||
</>
|
||||
}
|
||||
if (e?.length > 0) {
|
||||
return <>
|
||||
{e.map((item, i, { length: len}) => {
|
||||
if (i !== len - 1) {
|
||||
return <Box key={item}>
|
||||
<Text color="#17C083">{item}</Text>
|
||||
<Text dimColor>|</Text>
|
||||
</Box>
|
||||
}
|
||||
|
||||
return <Text color="#17C083" key={item}>{item}</Text>
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
return <Text color="#3894FF">string</Text>;
|
||||
}
|
||||
|
||||
const Command: FC<{ name: string, info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => {
|
||||
const Type: FC<{ type: any; enum?: string[] }> = ({ type, enum: e }) => {
|
||||
if (type === Boolean) {
|
||||
return (
|
||||
<Box display="flex" alignItems="flex-start">
|
||||
<Box width={24} display="flex" flexGrow={0}>
|
||||
<Text color="whiteBright">--{name}</Text>{alias && <Text dimColor> -{alias}</Text>}
|
||||
</Box>
|
||||
<Box width={24}>
|
||||
<Type type={type} enum={e} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<>
|
||||
<Text color="#3894FF">true</Text>
|
||||
<Text dimColor>|</Text>
|
||||
<Text color="#3894FF">false</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Help: FC<{ context: any }> = ({ context: { templates }}) => {
|
||||
}
|
||||
if (e?.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<Box width={48} display="flex" marginY={1}>
|
||||
<Text backgroundColor="#882DE7" color="white">{' astro '}</Text>
|
||||
<Box marginLeft={1}>
|
||||
<Text color="black" backgroundColor="white"> help </Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column">
|
||||
{Object.entries(ARGS).map(([name, info]) => <Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} /> )}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
<>
|
||||
{e.map((item, i, { length: len }) => {
|
||||
if (i !== len - 1) {
|
||||
return (
|
||||
<Box key={item}>
|
||||
<Text color="#17C083">{item}</Text>
|
||||
<Text dimColor>|</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text color="#17C083" key={item}>
|
||||
{item}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text color="#3894FF">string</Text>;
|
||||
};
|
||||
|
||||
const Command: FC<{ name: string; info: ARG }> = ({ name, info: { alias, description, type, enum: e } }) => {
|
||||
return (
|
||||
<Box display="flex" alignItems="flex-start">
|
||||
<Box width={24} display="flex" flexGrow={0}>
|
||||
<Text color="whiteBright">--{name}</Text>
|
||||
{alias && <Text dimColor> -{alias}</Text>}
|
||||
</Box>
|
||||
<Box width={24}>
|
||||
<Type type={type} enum={e} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Help: FC<{ context: any }> = ({ context: { templates } }) => {
|
||||
return (
|
||||
<>
|
||||
<Box width={48} display="flex" marginY={1}>
|
||||
<Text backgroundColor="#882DE7" color="white">
|
||||
{' astro '}
|
||||
</Text>
|
||||
<Box marginLeft={1}>
|
||||
<Text color="black" backgroundColor="white">
|
||||
{' '}
|
||||
help{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginBottom={1} marginLeft={2} display="flex" flexDirection="column">
|
||||
{Object.entries(ARGS).map(([name, info]) => (
|
||||
<Command key={name} name={name} info={name === 'template' ? { ...info, enum: templates.map(({ value }) => value) } : info} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Help;
|
||||
|
|
|
@ -4,16 +4,20 @@ import Spacer from './Spacer';
|
|||
import Spinner from './Spinner';
|
||||
|
||||
const Install: FC<{ context: any }> = ({ context: { use } }) => {
|
||||
return <>
|
||||
return (
|
||||
<>
|
||||
<Box display="flex">
|
||||
<Spinner/>
|
||||
<Spinner />
|
||||
<Text> Initiating launch sequence...</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Spacer />
|
||||
<Text color="white" dimColor>(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)</Text>
|
||||
<Text color="white" dimColor>
|
||||
(aka running <Text color="#17C083">{use === 'npm' ? 'npm install' : 'yarn'}</Text>)
|
||||
</Text>
|
||||
</Box>
|
||||
</>;
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Install;
|
||||
|
|
|
@ -8,8 +8,9 @@ const { default: Input } = TextInput;
|
|||
const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) => {
|
||||
const [value, setValue] = React.useState('');
|
||||
const handleSubmit = (v: string) => onSubmit(v);
|
||||
|
||||
return <>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex">
|
||||
<Text color="#17C083">{'[query]'}</Text>
|
||||
<Text> What is your project name?</Text>
|
||||
|
@ -18,7 +19,8 @@ const ProjectName: FC<{ onSubmit: (value: string) => void }> = ({ onSubmit }) =>
|
|||
<Spacer />
|
||||
<Input value={value} onChange={setValue} onSubmit={handleSubmit} placeholder="my-project" />
|
||||
</Box>
|
||||
</>;
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectName;
|
||||
|
|
|
@ -5,28 +5,28 @@ import { Text, Box } from 'ink';
|
|||
const { default: Select } = SelectInput;
|
||||
|
||||
interface Props {
|
||||
isSelected?: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
isSelected?: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
const Indicator: FC<Props> = ({ isSelected }) => isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text>
|
||||
const Item: FC<Props> = ({isSelected = false, label, description }) => (
|
||||
<Box display="flex">
|
||||
<Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>{label}</Text>
|
||||
{isSelected && description && typeof description === 'string' && <Text> {description}</Text>}
|
||||
{isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>}
|
||||
</Box>
|
||||
const Indicator: FC<Props> = ({ isSelected }) => (isSelected ? <Text color="#3894FF">[ </Text> : <Text> </Text>);
|
||||
const Item: FC<Props> = ({ isSelected = false, label, description }) => (
|
||||
<Box display="flex">
|
||||
<Text color={isSelected ? '#3894FF' : 'white'} dimColor={!isSelected}>
|
||||
{label}
|
||||
</Text>
|
||||
{isSelected && description && typeof description === 'string' && <Text> {description}</Text>}
|
||||
{isSelected && description && typeof description !== 'string' && <Box marginLeft={1}>{description}</Box>}
|
||||
</Box>
|
||||
);
|
||||
|
||||
interface SelectProps {
|
||||
items: { value: string|number|boolean, label: string, description?: any }[]
|
||||
onSelect(value: string|number|boolean): void;
|
||||
items: { value: string | number | boolean; label: string; description?: any }[];
|
||||
onSelect(value: string | number | boolean): void;
|
||||
}
|
||||
const CustomSelect: FC<SelectProps> = ({ items, onSelect }) => {
|
||||
const handleSelect = ({ value }) => onSelect(value);
|
||||
return (
|
||||
<Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} />
|
||||
)
|
||||
}
|
||||
const handleSelect = ({ value }) => onSelect(value);
|
||||
return <Select indicatorComponent={Indicator} itemComponent={Item} items={items} onSelect={handleSelect} />;
|
||||
};
|
||||
|
||||
export default CustomSelect;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Box } from 'ink';
|
||||
|
||||
const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} />
|
||||
const Spacer: FC<{ width?: number }> = ({ width = 8 }) => <Box width={width} />;
|
||||
export default Spacer;
|
||||
|
|
|
@ -6,14 +6,14 @@ const Spinner: FC<{ type?: keyof typeof spinners }> = ({ type = 'countdown' }) =
|
|||
const [i, setI] = useState(0);
|
||||
useEffect(() => {
|
||||
const _ = setInterval(() => {
|
||||
setI(v => (v < frames.length - 1) ? v + 1 : 0)
|
||||
}, interval)
|
||||
setI((v) => (v < frames.length - 1 ? v + 1 : 0));
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(_);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return frames[i]
|
||||
}
|
||||
return frames[i];
|
||||
};
|
||||
|
||||
const spinners = {
|
||||
countdown: {
|
||||
|
@ -35,73 +35,73 @@ const spinners = {
|
|||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#882DE7"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
|
@ -122,79 +122,79 @@ const spinners = {
|
|||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#17C083">{' '}</Text>
|
||||
<Text backgroundColor="#882DE7"> </Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
<Text backgroundColor="#17C083"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#23B1AF">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
<Text backgroundColor="#23B1AF"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#2CA5D2">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
<Text backgroundColor="#2CA5D2"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#3894FF">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
<Text backgroundColor="#3894FF"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#5076F9">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
<Text backgroundColor="#5076F9"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1">{' '}</Text>
|
||||
<Text backgroundColor="#6858F1"> </Text>
|
||||
</Box>,
|
||||
<Box display="flex">
|
||||
<Text backgroundColor="#882DE7">{' '}</Text>
|
||||
</Box>,
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Box, Text } from 'ink';
|
|||
import Spacer from './Spacer';
|
||||
import Select from './Select';
|
||||
|
||||
const Template: FC<{ context: any, onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => {
|
||||
const Template: FC<{ context: any; onSubmit: (value: string) => void }> = ({ context: { templates }, onSubmit }) => {
|
||||
const items = templates.map(({ title: label, ...rest }) => ({ ...rest, label }));
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
import type * as arg from 'arg';
|
||||
|
||||
export interface ARG {
|
||||
type: any;
|
||||
description: string;
|
||||
enum?: string[];
|
||||
alias?: string;
|
||||
type: any;
|
||||
description: string;
|
||||
enum?: string[];
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export const ARGS: Record<string, ARG> = {
|
||||
'template': {
|
||||
type: String,
|
||||
description: 'specifies template to use'
|
||||
},
|
||||
'use': {
|
||||
type: String,
|
||||
enum: ['npm', 'yarn'],
|
||||
description: 'specifies package manager to use'
|
||||
},
|
||||
'run': {
|
||||
type: Boolean,
|
||||
description: 'should dependencies be installed automatically?'
|
||||
},
|
||||
'force': {
|
||||
type: Boolean,
|
||||
alias: 'f',
|
||||
description: 'should existing files be overwritten?'
|
||||
},
|
||||
'version': {
|
||||
type: Boolean,
|
||||
alias: 'v',
|
||||
description: 'prints current version'
|
||||
},
|
||||
'help': {
|
||||
type: Boolean,
|
||||
alias: 'h',
|
||||
description: 'prints this message'
|
||||
}
|
||||
}
|
||||
template: {
|
||||
type: String,
|
||||
description: 'specifies template to use',
|
||||
},
|
||||
use: {
|
||||
type: String,
|
||||
enum: ['npm', 'yarn'],
|
||||
description: 'specifies package manager to use',
|
||||
},
|
||||
run: {
|
||||
type: Boolean,
|
||||
description: 'should dependencies be installed automatically?',
|
||||
},
|
||||
force: {
|
||||
type: Boolean,
|
||||
alias: 'f',
|
||||
description: 'should existing files be overwritten?',
|
||||
},
|
||||
version: {
|
||||
type: Boolean,
|
||||
alias: 'v',
|
||||
description: 'prints current version',
|
||||
},
|
||||
help: {
|
||||
type: Boolean,
|
||||
alias: 'h',
|
||||
description: 'prints this message',
|
||||
},
|
||||
};
|
||||
|
||||
export const args = Object.entries(ARGS).reduce((acc, [name, info]) => {
|
||||
const key = `--${name}`;
|
||||
const spec = { ...acc, [key]: info.type };
|
||||
const key = `--${name}`;
|
||||
const spec = { ...acc, [key]: info.type };
|
||||
|
||||
if (info.alias) {
|
||||
spec[`-${info.alias}`] = key;
|
||||
}
|
||||
return spec
|
||||
if (info.alias) {
|
||||
spec[`-${info.alias}`] = key;
|
||||
}
|
||||
return spec;
|
||||
}, {} as arg.Spec);
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import App from './components/App';
|
||||
import Version from './components/Version';
|
||||
import Exit from './components/Exit';
|
||||
import {render} from 'ink';
|
||||
import { render } from 'ink';
|
||||
import { getTemplates, addProcessListeners } from './utils';
|
||||
import { args as argsConfig } from './config';
|
||||
import arg from 'arg';
|
||||
|
@ -11,36 +11,36 @@ import Help from './components/Help';
|
|||
|
||||
/** main `create-astro` CLI */
|
||||
export default async function createAstro() {
|
||||
const args = arg(argsConfig);
|
||||
const projectName = args._[0];
|
||||
if (args['--version']) {
|
||||
return render(<Version />);
|
||||
}
|
||||
const templates = await getTemplates();
|
||||
if (args['--help']) {
|
||||
return render(<Help context={{ templates }} />)
|
||||
}
|
||||
const args = arg(argsConfig);
|
||||
const projectName = args._[0];
|
||||
if (args['--version']) {
|
||||
return render(<Version />);
|
||||
}
|
||||
const templates = await getTemplates();
|
||||
if (args['--help']) {
|
||||
return render(<Help context={{ templates }} />);
|
||||
}
|
||||
|
||||
const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm';
|
||||
const use = (args['--use'] ?? pkgManager) as 'npm'|'yarn';
|
||||
const template = args['--template'];
|
||||
const force = args['--force'];
|
||||
const run = args['--run'] ?? true;
|
||||
|
||||
const app = render(<App context={{ projectName, template, templates, force, run, use }} />);
|
||||
const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm';
|
||||
const use = (args['--use'] ?? pkgManager) as 'npm' | 'yarn';
|
||||
const template = args['--template'];
|
||||
const force = args['--force'];
|
||||
const run = args['--run'] ?? true;
|
||||
|
||||
const onError = () => {
|
||||
if (app) app.clear();
|
||||
render(<Exit didError />);
|
||||
}
|
||||
const onExit = () => {
|
||||
if (app) app.clear();
|
||||
render(<Exit />);
|
||||
}
|
||||
addProcessListeners([
|
||||
['uncaughtException', onError],
|
||||
['exit', onExit],
|
||||
['SIGINT', onExit],
|
||||
['SIGTERM', onExit],
|
||||
])
|
||||
const app = render(<App context={{ projectName, template, templates, force, run, use }} />);
|
||||
|
||||
const onError = () => {
|
||||
if (app) app.clear();
|
||||
render(<Exit didError />);
|
||||
};
|
||||
const onExit = () => {
|
||||
if (app) app.clear();
|
||||
render(<Exit />);
|
||||
};
|
||||
addProcessListeners([
|
||||
['uncaughtException', onError],
|
||||
['exit', onExit],
|
||||
['SIGINT', onExit],
|
||||
['SIGTERM', onExit],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"title": "Blank",
|
||||
"description": "a bare-bones, ultra-minimal template"
|
||||
"title": "Blank",
|
||||
"description": "a bare-bones, ultra-minimal template"
|
||||
}
|
||||
|
|
|
@ -20,10 +20,8 @@ Inside of your Astro project, you'll see the following folders and files:
|
|||
```
|
||||
|
||||
Astro looks for `.astro` or `.md.astro` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"title": "Getting Started",
|
||||
"description": "a friendly starting point for new astronauts",
|
||||
"rank": 999
|
||||
"title": "Getting Started",
|
||||
"description": "a friendly starting point for new astronauts",
|
||||
"rank": 999
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import decompress from 'decompress';
|
|||
|
||||
const listeners = new Map();
|
||||
|
||||
export async function addProcessListeners(handlers: [NodeJS.Signals|string, NodeJS.SignalsListener][]) {
|
||||
for (const [event,handler] of handlers) {
|
||||
export async function addProcessListeners(handlers: [NodeJS.Signals | string, NodeJS.SignalsListener][]) {
|
||||
for (const [event, handler] of handlers) {
|
||||
listeners.set(event, handler);
|
||||
process.once(event as NodeJS.Signals, handler);
|
||||
}
|
||||
|
@ -21,18 +21,20 @@ export async function cancelProcessListeners() {
|
|||
}
|
||||
|
||||
export async function getTemplates() {
|
||||
const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url));
|
||||
const templateFiles = await fs.readdir(templatesRoot, 'utf8');
|
||||
const templates = templateFiles.filter(t => t.endsWith('.tgz'));
|
||||
const metafile = templateFiles.find(t => t.endsWith('meta.json'));
|
||||
const templatesRoot = fileURLToPath(new URL('./templates', import.meta.url));
|
||||
const templateFiles = await fs.readdir(templatesRoot, 'utf8');
|
||||
const templates = templateFiles.filter((t) => t.endsWith('.tgz'));
|
||||
const metafile = templateFiles.find((t) => t.endsWith('meta.json'));
|
||||
|
||||
const meta = await fs.readFile(resolve(templatesRoot, metafile)).then(r => JSON.parse(r.toString()));
|
||||
const meta = await fs.readFile(resolve(templatesRoot, metafile)).then((r) => JSON.parse(r.toString()));
|
||||
|
||||
return templates.map(template => {
|
||||
return templates
|
||||
.map((template) => {
|
||||
const value = basename(template, '.tgz');
|
||||
if (meta[value]) return { ...meta[value], value };
|
||||
return { value };
|
||||
}).sort((a, b) => {
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aRank = a.rank ?? 0;
|
||||
const bRank = b.rank ?? 0;
|
||||
if (aRank > bRank) return -1;
|
||||
|
@ -49,28 +51,29 @@ export async function rewriteFiles(projectName: string) {
|
|||
const tasks = [];
|
||||
tasks.push(fs.rename(resolve(dest, '_gitignore'), resolve(dest, '.gitignore')));
|
||||
tasks.push(
|
||||
fs.readFile(resolve(dest, 'package.json'))
|
||||
.then(res => JSON.parse(res.toString()))
|
||||
.then(json => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2))
|
||||
.then(res => fs.writeFile(resolve(dest, 'package.json'), res))
|
||||
fs
|
||||
.readFile(resolve(dest, 'package.json'))
|
||||
.then((res) => JSON.parse(res.toString()))
|
||||
.then((json) => JSON.stringify({ ...json, name: getValidPackageName(projectName) }, null, 2))
|
||||
.then((res) => fs.writeFile(resolve(dest, 'package.json'), res))
|
||||
);
|
||||
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
export async function prepareTemplate(use: 'npm'|'yarn', name: string, dest: string) {
|
||||
const projectName = dest;
|
||||
dest = resolve(dest);
|
||||
const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url));
|
||||
await decompress(template, dest);
|
||||
await rewriteFiles(projectName);
|
||||
try {
|
||||
await run(use, use === 'npm' ? 'i' : null, dest);
|
||||
} catch (e) {
|
||||
cleanup(true);
|
||||
}
|
||||
isDone = true;
|
||||
return;
|
||||
export async function prepareTemplate(use: 'npm' | 'yarn', name: string, dest: string) {
|
||||
const projectName = dest;
|
||||
dest = resolve(dest);
|
||||
const template = fileURLToPath(new URL(`./templates/${name}.tgz`, import.meta.url));
|
||||
await decompress(template, dest);
|
||||
await rewriteFiles(projectName);
|
||||
try {
|
||||
await run(use, use === 'npm' ? 'i' : null, dest);
|
||||
} catch (e) {
|
||||
cleanup(true);
|
||||
}
|
||||
isDone = true;
|
||||
return;
|
||||
}
|
||||
|
||||
export function cleanup(didError = false) {
|
||||
|
@ -81,10 +84,10 @@ export function cleanup(didError = false) {
|
|||
}
|
||||
|
||||
export function killChildren() {
|
||||
childrenProcesses.forEach(p => p.kill('SIGINT'));
|
||||
childrenProcesses.forEach((p) => p.kill('SIGINT'));
|
||||
}
|
||||
|
||||
export function run(pkgManager: 'npm'|'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> {
|
||||
export function run(pkgManager: 'npm' | 'yarn', command: string, projectPath: string, stdio: any = 'ignore'): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = spawn(pkgManager, command ? [command] : [], {
|
||||
shell: true,
|
||||
|
@ -102,40 +105,40 @@ export function isWin() {
|
|||
}
|
||||
|
||||
export function isEmpty(path) {
|
||||
try {
|
||||
const files = readdirSync(resolve(path));
|
||||
if (files.length > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
try {
|
||||
const files = readdirSync(resolve(path));
|
||||
if (files.length > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function emptyDir(dir) {
|
||||
dir = resolve(dir);
|
||||
if (!existsSync(dir)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
for (const file of readdirSync(dir)) {
|
||||
const abs = resolve(dir, file)
|
||||
const abs = resolve(dir, file);
|
||||
if (lstatSync(abs).isDirectory()) {
|
||||
emptyDir(abs)
|
||||
rmdirSync(abs)
|
||||
emptyDir(abs);
|
||||
rmdirSync(abs);
|
||||
} else {
|
||||
unlinkSync(abs)
|
||||
unlinkSync(abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getValidPackageName(projectName: string) {
|
||||
const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
|
||||
const packageNameRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
||||
|
||||
if (packageNameRegExp.test(projectName)) {
|
||||
return projectName
|
||||
return projectName;
|
||||
}
|
||||
|
||||
return projectName
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "astro-scripts",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"astro-scripts": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"arg": "^5.0.0",
|
||||
"esbuild": "^0.11.16",
|
||||
"globby": "^11.0.3",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
"name": "astro-scripts",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"astro-scripts": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"arg": "^5.0.0",
|
||||
"esbuild": "^0.11.16",
|
||||
"globby": "^11.0.3",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,57 +4,58 @@ import { relative, isAbsolute, join, dirname } from 'path';
|
|||
import { promises as fs } from 'fs';
|
||||
|
||||
const convertMessage = ({ message, start, end, filename, frame }) => ({
|
||||
text: message,
|
||||
location: start && end && {
|
||||
file: filename,
|
||||
line: start.line,
|
||||
column: start.column,
|
||||
length: start.line === end.line ? end.column - start.column : 0,
|
||||
lineText: frame,
|
||||
text: message,
|
||||
location: start &&
|
||||
end && {
|
||||
file: filename,
|
||||
line: start.line,
|
||||
column: start.column,
|
||||
length: start.line === end.line ? end.column - start.column : 0,
|
||||
lineText: frame,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleLoad = async (args, generate) => {
|
||||
const { path } = args;
|
||||
const source = await fs.readFile(path, 'utf8');
|
||||
const filename = relative(process.cwd(), path)
|
||||
const { path } = args;
|
||||
const source = await fs.readFile(path, 'utf8');
|
||||
const filename = relative(process.cwd(), path);
|
||||
|
||||
try {
|
||||
let compileOptions = { css: false, generate, hydratable: true };
|
||||
try {
|
||||
let compileOptions = { css: false, generate, hydratable: true };
|
||||
|
||||
let { js, warnings } = compile(source, { ...compileOptions, filename })
|
||||
let contents = js.code + `\n//# sourceMappingURL=` + js.map.toUrl()
|
||||
let { js, warnings } = compile(source, { ...compileOptions, filename });
|
||||
let contents = js.code + `\n//# sourceMappingURL=` + js.map.toUrl();
|
||||
|
||||
return { loader: 'js', contents, resolveDir: dirname(path), warnings: warnings.map(w => convertMessage(w)) };
|
||||
} catch (e) {
|
||||
return { errors: [convertMessage(e)] }
|
||||
}
|
||||
}
|
||||
return { loader: 'js', contents, resolveDir: dirname(path), warnings: warnings.map((w) => convertMessage(w)) };
|
||||
} catch (e) {
|
||||
return { errors: [convertMessage(e)] };
|
||||
}
|
||||
};
|
||||
|
||||
export default function sveltePlugin() {
|
||||
return {
|
||||
name: 'svelte-esbuild',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /\.svelte$/ }, args => {
|
||||
let path = args.path.replace(/\.(?:client|server)/, '');
|
||||
path = isAbsolute(path) ? path : join(args.resolveDir, path)
|
||||
|
||||
if (/\.client\.svelte$/.test(args.path)) {
|
||||
return {
|
||||
path,
|
||||
namespace: 'svelte:client',
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: 'svelte-esbuild',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /\.svelte$/ }, (args) => {
|
||||
let path = args.path.replace(/\.(?:client|server)/, '');
|
||||
path = isAbsolute(path) ? path : join(args.resolveDir, path);
|
||||
|
||||
if (/\.server\.svelte$/.test(args.path)) {
|
||||
return {
|
||||
path,
|
||||
namespace: 'svelte:server',
|
||||
}
|
||||
}
|
||||
});
|
||||
build.onLoad({ filter: /.*/, namespace: 'svelte:client' }, (args) => handleLoad(args, 'dom'))
|
||||
build.onLoad({ filter: /.*/, namespace: 'svelte:server' }, (args) => handleLoad(args, 'ssr'))
|
||||
},
|
||||
}
|
||||
if (/\.client\.svelte$/.test(args.path)) {
|
||||
return {
|
||||
path,
|
||||
namespace: 'svelte:client',
|
||||
};
|
||||
}
|
||||
|
||||
if (/\.server\.svelte$/.test(args.path)) {
|
||||
return {
|
||||
path,
|
||||
namespace: 'svelte:server',
|
||||
};
|
||||
}
|
||||
});
|
||||
build.onLoad({ filter: /.*/, namespace: 'svelte:client' }, (args) => handleLoad(args, 'dom'));
|
||||
build.onLoad({ filter: /.*/, namespace: 'svelte:server' }, (args) => handleLoad(args, 'ssr'));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ module.exports.parsers = {
|
|||
return node.end;
|
||||
},
|
||||
astFormat: 'astro-expression',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const findExpressionsInAST = (node, collect = []) => {
|
||||
|
@ -50,24 +50,24 @@ const findExpressionsInAST = (node, collect = []) => {
|
|||
return collect.concat(node);
|
||||
}
|
||||
if (node.children) {
|
||||
collect.push(...[].concat(...node.children.map(child => findExpressionsInAST(child))));
|
||||
collect.push(...[].concat(...node.children.map((child) => findExpressionsInAST(child))));
|
||||
}
|
||||
return collect;
|
||||
}
|
||||
};
|
||||
|
||||
const formatExpression = ({ expression: { codeChunks, children }}, text, options) => {
|
||||
const formatExpression = ({ expression: { codeChunks, children } }, text, options) => {
|
||||
if (children.length === 0) {
|
||||
const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk.
|
||||
if (codeStart && [`'`, `"`].includes(codeStart[0])) {
|
||||
return `<script $ lang="ts">${codeChunks.join('')}</script>`
|
||||
return `<script $ lang="ts">${codeChunks.join('')}</script>`;
|
||||
}
|
||||
return `{${codeChunks.join('')}}`;
|
||||
}
|
||||
|
||||
return `<script $ lang="ts">${text}</script>`;
|
||||
}
|
||||
};
|
||||
|
||||
const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find(v => v === '$');
|
||||
const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find((v) => v === '$');
|
||||
|
||||
const walkDoc = (doc) => {
|
||||
let inAstroScript = false;
|
||||
|
@ -77,38 +77,38 @@ const walkDoc = (doc) => {
|
|||
inAstroScript = true;
|
||||
parent.contents = { type: 'concat', parts: ['{'] };
|
||||
}
|
||||
return node.parts.map(part => recurse(part, { parent: node }));
|
||||
return node.parts.map((part) => recurse(part, { parent: node }));
|
||||
}
|
||||
if (inAstroScript) {
|
||||
if (node.type === 'break-parent') {
|
||||
parent.parts = parent.parts.filter(part => !['break-parent', 'line'].includes(part.type));
|
||||
parent.parts = parent.parts.filter((part) => !['break-parent', 'line'].includes(part.type));
|
||||
}
|
||||
if (node.type === 'indent') {
|
||||
parent.parts = parent.parts.map(part => {
|
||||
parent.parts = parent.parts.map((part) => {
|
||||
if (part.type !== 'indent') return part;
|
||||
return {
|
||||
type: 'concat',
|
||||
parts: [part.contents]
|
||||
}
|
||||
})
|
||||
parts: [part.contents],
|
||||
};
|
||||
});
|
||||
}
|
||||
if (typeof node === 'string' && node.endsWith(';')) {
|
||||
parent.parts = parent.parts.map(part => {
|
||||
parent.parts = parent.parts.map((part) => {
|
||||
if (typeof part === 'string' && part.endsWith(';')) return part.slice(0, -1);
|
||||
return part;
|
||||
});
|
||||
}
|
||||
if (node === '</script>') {
|
||||
parent.parts = parent.parts.map(part => part === '</script>' ? '}' : part);
|
||||
parent.parts = parent.parts.map((part) => (part === '</script>' ? '}' : part));
|
||||
inAstroScript = false;
|
||||
}
|
||||
}
|
||||
if (['group', 'indent'].includes(node.type)) {
|
||||
return recurse(node.contents, { parent: node });
|
||||
}
|
||||
}
|
||||
};
|
||||
recurse(doc, { parent: null });
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {Record<string, import('prettier').Printer>} */
|
||||
module.exports.printers = {
|
||||
|
@ -129,18 +129,20 @@ module.exports.printers = {
|
|||
if (node.type === 'Fragment' && node.isRoot) {
|
||||
const expressions = findExpressionsInAST(node);
|
||||
if (expressions.length > 0) {
|
||||
const parts = [].concat(...expressions.map((expr, i, all) => {
|
||||
const prev = all[i - 1];
|
||||
const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start);
|
||||
const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options);
|
||||
const parts = [].concat(
|
||||
...expressions.map((expr, i, all) => {
|
||||
const prev = all[i - 1];
|
||||
const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start);
|
||||
const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options);
|
||||
|
||||
if (i === all.length - 1) {
|
||||
const end = node.text.slice(expr.end - node.start);
|
||||
return [start, exprText, end]
|
||||
}
|
||||
if (i === all.length - 1) {
|
||||
const end = node.text.slice(expr.end - node.start);
|
||||
return [start, exprText, end];
|
||||
}
|
||||
|
||||
return [start, exprText]
|
||||
}));
|
||||
return [start, exprText];
|
||||
})
|
||||
);
|
||||
const html = parts.join('\n');
|
||||
const doc = textToDoc(html, { parser: 'html' });
|
||||
walkDoc(doc);
|
||||
|
|
|
@ -2,10 +2,10 @@ import { suite } from 'uvu';
|
|||
import * as assert from 'uvu/assert';
|
||||
import { format } from './test-utils.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import { fileURLToPath } from 'url'
|
||||
import { fileURLToPath } from 'url';
|
||||
const Prettier = suite('Prettier formatting');
|
||||
|
||||
const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then(res => res.toString())
|
||||
const readFile = (path) => fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url))).then((res) => res.toString());
|
||||
|
||||
/**
|
||||
* Utility to get `[src, out]` files
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
{
|
||||
"comments": {
|
||||
"blockComment": [ "<!--", "-->" ]
|
||||
},
|
||||
"brackets": [
|
||||
["---", "---"],
|
||||
["<!--", "-->"],
|
||||
["<", ">"],
|
||||
["{", "}"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}"},
|
||||
{ "open": "[", "close": "]"},
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "'", "close": "'" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "<!--", "close": "-->", "notIn": [ "comment", "string" ]},
|
||||
{ "open": "/**", "close": " */", "notIn": ["string"] }
|
||||
],
|
||||
"autoCloseBefore": ";:.,=}])>` \n\t",
|
||||
"surroundingPairs": [
|
||||
{ "open": "'", "close": "'" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "{", "close": "}"},
|
||||
{ "open": "[", "close": "]"},
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "<", "close": ">" }
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*<!--\\s*#region\\b.*-->",
|
||||
"end": "^\\s*<!--\\s*#endregion\\b.*-->"
|
||||
}
|
||||
}
|
||||
"comments": {
|
||||
"blockComment": ["<!--", "-->"]
|
||||
},
|
||||
"brackets": [
|
||||
["---", "---"],
|
||||
["<!--", "-->"],
|
||||
["<", ">"],
|
||||
["{", "}"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "'", "close": "'" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "<!--", "close": "-->", "notIn": ["comment", "string"] },
|
||||
{ "open": "/**", "close": " */", "notIn": ["string"] }
|
||||
],
|
||||
"autoCloseBefore": ";:.,=}])>` \n\t",
|
||||
"surroundingPairs": [
|
||||
{ "open": "'", "close": "'" },
|
||||
{ "open": "\"", "close": "\"" },
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "<", "close": ">" }
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*<!--\\s*#region\\b.*-->",
|
||||
"end": "^\\s*<!--\\s*#endregion\\b.*-->"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +1,96 @@
|
|||
{
|
||||
"name": "@astro.build/vscode",
|
||||
"displayName": "Astro",
|
||||
"description": "Language support for Astro",
|
||||
"icon": "assets/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FF5D01",
|
||||
"theme": "dark"
|
||||
},
|
||||
"version": "0.3.0",
|
||||
"author": "Astro",
|
||||
"publisher": "astro-build",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run build",
|
||||
"bootstrap": "cd packages/client && yarn && cd ../server && yarn",
|
||||
"build": "node scripts/build.mjs",
|
||||
"watch": "node scripts/watch.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.52.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:astro"
|
||||
],
|
||||
"dependencies": {
|
||||
"vscode-html-languageservice": "^3.0.3",
|
||||
"vscode-emmet-helper": "2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.10.0",
|
||||
"@astro-vscode/client": "file:./packages/client",
|
||||
"@astro-vscode/server": "file:./packages/server"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"languages/",
|
||||
"syntaxes/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"directory": "vscode",
|
||||
"url": "https://github.com/snowpackjs/astro"
|
||||
},
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Astro configuration",
|
||||
"properties": {
|
||||
"astro.trace.server": {
|
||||
"scope": "window",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Traces the communication between VS Code and the language server."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "astro",
|
||||
"extensions": [
|
||||
".astro"
|
||||
],
|
||||
"aliases": [
|
||||
"Astro"
|
||||
],
|
||||
"configuration": "./languages/astro-language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "astro",
|
||||
"scopeName": "text.html.astro",
|
||||
"path": "./syntaxes/astro.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html.astro": "astro",
|
||||
"text.html": "html",
|
||||
"source.css": "css",
|
||||
"source.scss": "scss",
|
||||
"source.sass": "sass",
|
||||
"source.tsx": "typescriptreact",
|
||||
"meta.embedded.block.frontmatter": "typescriptreact"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"name": "@astro.build/vscode",
|
||||
"displayName": "Astro",
|
||||
"description": "Language support for Astro",
|
||||
"icon": "assets/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FF5D01",
|
||||
"theme": "dark"
|
||||
},
|
||||
"version": "0.3.0",
|
||||
"author": "Astro",
|
||||
"publisher": "astro-build",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run build",
|
||||
"bootstrap": "cd packages/client && yarn && cd ../server && yarn",
|
||||
"build": "node scripts/build.mjs",
|
||||
"watch": "node scripts/watch.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.52.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:astro"
|
||||
],
|
||||
"dependencies": {
|
||||
"vscode-html-languageservice": "^3.0.3",
|
||||
"vscode-emmet-helper": "2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.10.0",
|
||||
"@astro-vscode/client": "file:./packages/client",
|
||||
"@astro-vscode/server": "file:./packages/server"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"languages/",
|
||||
"syntaxes/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"directory": "vscode",
|
||||
"url": "https://github.com/snowpackjs/astro"
|
||||
},
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Astro configuration",
|
||||
"properties": {
|
||||
"astro.trace.server": {
|
||||
"scope": "window",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Traces the communication between VS Code and the language server."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languages": [
|
||||
{
|
||||
"id": "astro",
|
||||
"extensions": [
|
||||
".astro"
|
||||
],
|
||||
"aliases": [
|
||||
"Astro"
|
||||
],
|
||||
"configuration": "./languages/astro-language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "astro",
|
||||
"scopeName": "text.html.astro",
|
||||
"path": "./syntaxes/astro.tmLanguage.json",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"text.html.astro": "astro",
|
||||
"text.html": "html",
|
||||
"source.css": "css",
|
||||
"source.scss": "scss",
|
||||
"source.sass": "sass",
|
||||
"source.tsx": "typescriptreact",
|
||||
"meta.embedded.block.frontmatter": "typescriptreact"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@astro-vscode/client",
|
||||
"version": "0.1.0",
|
||||
"author": "Skypack",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "latest"
|
||||
}
|
||||
"name": "@astro-vscode/client",
|
||||
"version": "0.1.0",
|
||||
"author": "Skypack",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,97 +12,78 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
|
|||
|
||||
/** */
|
||||
export function activateTagClosing(
|
||||
tagProvider: (document: TextDocument, position: Position) => Thenable<string>,
|
||||
supportedLanguages: { [id: string]: boolean },
|
||||
configName: string
|
||||
tagProvider: (document: TextDocument, position: Position) => Thenable<string>,
|
||||
supportedLanguages: { [id: string]: boolean },
|
||||
configName: string
|
||||
): Disposable {
|
||||
const disposables: Disposable[] = [];
|
||||
workspace.onDidChangeTextDocument(
|
||||
(event) => onDidChangeTextDocument(event.document, event.contentChanges),
|
||||
null,
|
||||
disposables
|
||||
);
|
||||
const disposables: Disposable[] = [];
|
||||
workspace.onDidChangeTextDocument((event) => onDidChangeTextDocument(event.document, event.contentChanges), null, disposables);
|
||||
|
||||
let isEnabled = false;
|
||||
updateEnabledState();
|
||||
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
|
||||
let isEnabled = false;
|
||||
updateEnabledState();
|
||||
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
|
||||
|
||||
let timeout: NodeJS.Timer | undefined = void 0;
|
||||
let timeout: NodeJS.Timer | undefined = void 0;
|
||||
|
||||
/** Check if this feature is enabled */
|
||||
function updateEnabledState() {
|
||||
isEnabled = false;
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const document = editor.document;
|
||||
if (!supportedLanguages[document.languageId]) {
|
||||
return;
|
||||
}
|
||||
if (!workspace.getConfiguration(void 0, document.uri).get<boolean>(configName)) {
|
||||
return;
|
||||
}
|
||||
isEnabled = true;
|
||||
/** Check if this feature is enabled */
|
||||
function updateEnabledState() {
|
||||
isEnabled = false;
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Handle text document changes */
|
||||
function onDidChangeTextDocument(
|
||||
document: TextDocument,
|
||||
changes: readonly TextDocumentContentChangeEvent[]
|
||||
) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
|
||||
if (document !== activeDocument || changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (typeof timeout !== 'undefined') {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
const lastChange = changes[changes.length - 1];
|
||||
const lastCharacter = lastChange.text[lastChange.text.length - 1];
|
||||
if (
|
||||
('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) ||
|
||||
(lastCharacter !== '>' && lastCharacter !== '/')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const rangeStart =
|
||||
'range' in lastChange
|
||||
? lastChange.range.start
|
||||
: new Position(0, document.getText().length);
|
||||
const version = document.version;
|
||||
timeout = setTimeout(() => {
|
||||
const position = new Position(
|
||||
rangeStart.line,
|
||||
rangeStart.character + lastChange.text.length
|
||||
);
|
||||
tagProvider(document, position).then((text) => {
|
||||
if (text && isEnabled) {
|
||||
const activeEditor = window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
const activeDocument = activeEditor.document;
|
||||
if (document === activeDocument && activeDocument.version === version) {
|
||||
const selections = activeEditor.selections;
|
||||
if (
|
||||
selections.length &&
|
||||
selections.some((s) => s.active.isEqual(position))
|
||||
) {
|
||||
activeEditor.insertSnippet(
|
||||
new SnippetString(text),
|
||||
selections.map((s) => s.active)
|
||||
);
|
||||
} else {
|
||||
activeEditor.insertSnippet(new SnippetString(text), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
timeout = void 0;
|
||||
}, 100);
|
||||
const document = editor.document;
|
||||
if (!supportedLanguages[document.languageId]) {
|
||||
return;
|
||||
}
|
||||
return Disposable.from(...disposables);
|
||||
if (!workspace.getConfiguration(void 0, document.uri).get<boolean>(configName)) {
|
||||
return;
|
||||
}
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
/** Handle text document changes */
|
||||
function onDidChangeTextDocument(document: TextDocument, changes: readonly TextDocumentContentChangeEvent[]) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
|
||||
if (document !== activeDocument || changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (typeof timeout !== 'undefined') {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
const lastChange = changes[changes.length - 1];
|
||||
const lastCharacter = lastChange.text[lastChange.text.length - 1];
|
||||
if (('range' in lastChange && (lastChange.rangeLength ?? 0) > 0) || (lastCharacter !== '>' && lastCharacter !== '/')) {
|
||||
return;
|
||||
}
|
||||
const rangeStart = 'range' in lastChange ? lastChange.range.start : new Position(0, document.getText().length);
|
||||
const version = document.version;
|
||||
timeout = setTimeout(() => {
|
||||
const position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
|
||||
tagProvider(document, position).then((text) => {
|
||||
if (text && isEnabled) {
|
||||
const activeEditor = window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
const activeDocument = activeEditor.document;
|
||||
if (document === activeDocument && activeDocument.version === version) {
|
||||
const selections = activeEditor.selections;
|
||||
if (selections.length && selections.some((s) => s.active.isEqual(position))) {
|
||||
activeEditor.insertSnippet(
|
||||
new SnippetString(text),
|
||||
selections.map((s) => s.active)
|
||||
);
|
||||
} else {
|
||||
activeEditor.insertSnippet(new SnippetString(text), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
timeout = void 0;
|
||||
}, 100);
|
||||
}
|
||||
return Disposable.from(...disposables);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [
|
||||
{ "path": "../server" }
|
||||
]
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{ "path": "../server" }]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { VSCodeEmmetConfig } from 'vscode-emmet-helper';
|
||||
|
||||
export class ConfigManager {
|
||||
private emmetConfig: VSCodeEmmetConfig = {};
|
||||
|
||||
updateEmmetConfig(config: VSCodeEmmetConfig): void {
|
||||
this.emmetConfig = config || {};
|
||||
}
|
||||
private emmetConfig: VSCodeEmmetConfig = {};
|
||||
|
||||
getEmmetConfig(): VSCodeEmmetConfig {
|
||||
return this.emmetConfig;
|
||||
}
|
||||
updateEmmetConfig(config: VSCodeEmmetConfig): void {
|
||||
this.emmetConfig = config || {};
|
||||
}
|
||||
|
||||
getEmmetConfig(): VSCodeEmmetConfig {
|
||||
return this.emmetConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,153 +7,147 @@ import { parseHtml } from './parseHtml';
|
|||
import { parseAstro, AstroDocument } from './parseAstro';
|
||||
|
||||
export class Document implements TextDocument {
|
||||
|
||||
private content: string;
|
||||
|
||||
languageId = 'astro';
|
||||
version = 0;
|
||||
html!: HTMLDocument;
|
||||
astro!: AstroDocument;
|
||||
private content: string;
|
||||
|
||||
constructor(public uri: string, text: string) {
|
||||
this.content = text;
|
||||
this.updateDocInfo();
|
||||
languageId = 'astro';
|
||||
version = 0;
|
||||
html!: HTMLDocument;
|
||||
astro!: AstroDocument;
|
||||
|
||||
constructor(public uri: string, text: string) {
|
||||
this.content = text;
|
||||
this.updateDocInfo();
|
||||
}
|
||||
|
||||
private updateDocInfo() {
|
||||
this.html = parseHtml(this.content);
|
||||
this.astro = parseAstro(this.content);
|
||||
}
|
||||
|
||||
setText(text: string) {
|
||||
this.content = text;
|
||||
this.version++;
|
||||
this.updateDocInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text between two positions.
|
||||
* @param text The new text slice
|
||||
* @param start Start offset of the new text
|
||||
* @param end End offset of the new text
|
||||
*/
|
||||
update(text: string, start: number, end: number): void {
|
||||
const content = this.getText();
|
||||
this.setText(content.slice(0, start) + text + content.slice(end));
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line and character based on the offset
|
||||
* @param offset The index of the position
|
||||
*/
|
||||
positionAt(offset: number): Position {
|
||||
offset = clamp(offset, 0, this.getTextLength());
|
||||
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
let low = 0;
|
||||
let high = lineOffsets.length;
|
||||
if (high === 0) {
|
||||
return Position.create(0, offset);
|
||||
}
|
||||
|
||||
private updateDocInfo() {
|
||||
this.html = parseHtml(this.content);
|
||||
this.astro = parseAstro(this.content);
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (lineOffsets[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
setText(text: string) {
|
||||
this.content = text;
|
||||
this.version++;
|
||||
this.updateDocInfo();
|
||||
// low is the least x for which the line offset is larger than the current offset
|
||||
// or array.length if no line offset is larger than the current offset
|
||||
const line = low - 1;
|
||||
return Position.create(line, offset - lineOffsets[line]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the line and character position
|
||||
* @param position Line and character position
|
||||
*/
|
||||
offsetAt(position: Position): number {
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
|
||||
if (position.line >= lineOffsets.length) {
|
||||
return this.getTextLength();
|
||||
} else if (position.line < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the text between two positions.
|
||||
* @param text The new text slice
|
||||
* @param start Start offset of the new text
|
||||
* @param end End offset of the new text
|
||||
*/
|
||||
update(text: string, start: number, end: number): void {
|
||||
const content = this.getText();
|
||||
this.setText(content.slice(0, start) + text + content.slice(end));
|
||||
const lineOffset = lineOffsets[position.line];
|
||||
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.getTextLength();
|
||||
|
||||
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
|
||||
}
|
||||
|
||||
getLineUntilOffset(offset: number): string {
|
||||
const { line, character } = this.positionAt(offset);
|
||||
return this.lines[line].slice(0, character);
|
||||
}
|
||||
|
||||
private getLineOffsets() {
|
||||
const lineOffsets = [];
|
||||
const text = this.getText();
|
||||
let isLineStart = true;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isLineStart) {
|
||||
lineOffsets.push(i);
|
||||
isLineStart = false;
|
||||
}
|
||||
const ch = text.charAt(i);
|
||||
isLineStart = ch === '\r' || ch === '\n';
|
||||
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return this.content
|
||||
if (isLineStart && text.length > 0) {
|
||||
lineOffsets.push(text.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line and character based on the offset
|
||||
* @param offset The index of the position
|
||||
*/
|
||||
positionAt(offset: number): Position {
|
||||
offset = clamp(offset, 0, this.getTextLength());
|
||||
return lineOffsets;
|
||||
}
|
||||
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
let low = 0;
|
||||
let high = lineOffsets.length;
|
||||
if (high === 0) {
|
||||
return Position.create(0, offset);
|
||||
}
|
||||
/**
|
||||
* Get the length of the document's content
|
||||
*/
|
||||
getTextLength(): number {
|
||||
return this.getText().length;
|
||||
}
|
||||
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (lineOffsets[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the file path if the url scheme is file
|
||||
*/
|
||||
getFilePath(): string | null {
|
||||
return urlToPath(this.uri);
|
||||
}
|
||||
|
||||
// low is the least x for which the line offset is larger than the current offset
|
||||
// or array.length if no line offset is larger than the current offset
|
||||
const line = low - 1;
|
||||
return Position.create(line, offset - lineOffsets[line]);
|
||||
}
|
||||
/**
|
||||
* Get URL file path.
|
||||
*/
|
||||
getURL() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the line and character position
|
||||
* @param position Line and character position
|
||||
*/
|
||||
offsetAt(position: Position): number {
|
||||
const lineOffsets = this.getLineOffsets();
|
||||
|
||||
if (position.line >= lineOffsets.length) {
|
||||
return this.getTextLength();
|
||||
} else if (position.line < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lineOffset = lineOffsets[position.line];
|
||||
const nextLineOffset =
|
||||
position.line + 1 < lineOffsets.length
|
||||
? lineOffsets[position.line + 1]
|
||||
: this.getTextLength();
|
||||
|
||||
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
|
||||
}
|
||||
|
||||
getLineUntilOffset(offset: number): string {
|
||||
const { line, character } = this.positionAt(offset);
|
||||
return this.lines[line].slice(0, character);
|
||||
}
|
||||
|
||||
private getLineOffsets() {
|
||||
const lineOffsets = [];
|
||||
const text = this.getText();
|
||||
let isLineStart = true;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isLineStart) {
|
||||
lineOffsets.push(i);
|
||||
isLineStart = false;
|
||||
}
|
||||
const ch = text.charAt(i);
|
||||
isLineStart = ch === '\r' || ch === '\n';
|
||||
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLineStart && text.length > 0) {
|
||||
lineOffsets.push(text.length);
|
||||
}
|
||||
|
||||
return lineOffsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the document's content
|
||||
*/
|
||||
getTextLength(): number {
|
||||
return this.getText().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file path if the url scheme is file
|
||||
*/
|
||||
getFilePath(): string | null {
|
||||
return urlToPath(this.uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL file path.
|
||||
*/
|
||||
getURL() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
|
||||
get lines(): string[] {
|
||||
return this.getText().split(/\r?\n/);
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
return this.lines.length;
|
||||
}
|
||||
get lines(): string[] {
|
||||
return this.getText().split(/\r?\n/);
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
return this.lines.length;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,104 +1,94 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
TextDocumentContentChangeEvent,
|
||||
TextDocumentItem
|
||||
} from 'vscode-languageserver';
|
||||
import { TextDocumentContentChangeEvent, TextDocumentItem } from 'vscode-languageserver';
|
||||
import { Document } from './Document';
|
||||
import { normalizeUri } from '../../utils';
|
||||
|
||||
export type DocumentEvent = 'documentOpen' | 'documentChange' | 'documentClose';
|
||||
|
||||
export class DocumentManager {
|
||||
private emitter = new EventEmitter();
|
||||
private openedInClient = new Set<string>();
|
||||
private documents: Map<string, Document> = new Map();
|
||||
private locked = new Set<string>();
|
||||
private deleteCandidates = new Set<string>();
|
||||
private emitter = new EventEmitter();
|
||||
private openedInClient = new Set<string>();
|
||||
private documents: Map<string, Document> = new Map();
|
||||
private locked = new Set<string>();
|
||||
private deleteCandidates = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private createDocument: (textDocument: { uri: string, text: string }) => Document
|
||||
) {}
|
||||
constructor(private createDocument: (textDocument: { uri: string; text: string }) => Document) {}
|
||||
|
||||
get(uri: string) {
|
||||
return this.documents.get(normalizeUri(uri));
|
||||
get(uri: string) {
|
||||
return this.documents.get(normalizeUri(uri));
|
||||
}
|
||||
|
||||
openDocument(textDocument: TextDocumentItem) {
|
||||
let document: Document;
|
||||
if (this.documents.has(textDocument.uri)) {
|
||||
document = this.get(textDocument.uri) as Document;
|
||||
document.setText(textDocument.text);
|
||||
} else {
|
||||
document = this.createDocument(textDocument);
|
||||
this.documents.set(normalizeUri(textDocument.uri), document);
|
||||
this.notify('documentOpen', document);
|
||||
}
|
||||
|
||||
openDocument(textDocument: TextDocumentItem) {
|
||||
let document: Document;
|
||||
if (this.documents.has(textDocument.uri)) {
|
||||
document = this.get(textDocument.uri) as Document;
|
||||
document.setText(textDocument.text);
|
||||
} else {
|
||||
document = this.createDocument(textDocument);
|
||||
this.documents.set(normalizeUri(textDocument.uri), document);
|
||||
this.notify('documentOpen', document);
|
||||
}
|
||||
this.notify('documentChange', document);
|
||||
|
||||
this.notify('documentChange', document);
|
||||
return document;
|
||||
}
|
||||
|
||||
return document;
|
||||
closeDocument(uri: string) {
|
||||
uri = normalizeUri(uri);
|
||||
|
||||
const document = this.documents.get(uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
closeDocument(uri: string) {
|
||||
uri = normalizeUri(uri);
|
||||
this.notify('documentClose', document);
|
||||
|
||||
const document = this.documents.get(uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
this.notify('documentClose', document);
|
||||
|
||||
// Some plugin may prevent a document from actually being closed.
|
||||
if (!this.locked.has(uri)) {
|
||||
this.documents.delete(uri);
|
||||
} else {
|
||||
this.deleteCandidates.add(uri);
|
||||
}
|
||||
|
||||
this.openedInClient.delete(uri);
|
||||
// Some plugin may prevent a document from actually being closed.
|
||||
if (!this.locked.has(uri)) {
|
||||
this.documents.delete(uri);
|
||||
} else {
|
||||
this.deleteCandidates.add(uri);
|
||||
}
|
||||
|
||||
updateDocument(
|
||||
uri: string,
|
||||
changes: TextDocumentContentChangeEvent[]
|
||||
) {
|
||||
const document = this.documents.get(normalizeUri(uri));
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
this.openedInClient.delete(uri);
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = document.offsetAt(change.range.start);
|
||||
end = document.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = document.getTextLength();
|
||||
}
|
||||
|
||||
document.update(change.text, start, end);
|
||||
}
|
||||
|
||||
this.notify('documentChange', document);
|
||||
updateDocument(uri: string, changes: TextDocumentContentChangeEvent[]) {
|
||||
const document = this.documents.get(normalizeUri(uri));
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
markAsOpenedInClient(uri: string) {
|
||||
this.openedInClient.add(normalizeUri(uri));
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = document.offsetAt(change.range.start);
|
||||
end = document.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = document.getTextLength();
|
||||
}
|
||||
|
||||
document.update(change.text, start, end);
|
||||
}
|
||||
|
||||
getAllOpenedByClient() {
|
||||
return Array.from(this.documents.entries()).filter((doc) =>
|
||||
this.openedInClient.has(doc[0])
|
||||
);
|
||||
}
|
||||
this.notify('documentChange', document);
|
||||
}
|
||||
|
||||
on(name: DocumentEvent, listener: (document: Document) => void) {
|
||||
this.emitter.on(name, listener);
|
||||
}
|
||||
markAsOpenedInClient(uri: string) {
|
||||
this.openedInClient.add(normalizeUri(uri));
|
||||
}
|
||||
|
||||
private notify(name: DocumentEvent, document: Document) {
|
||||
this.emitter.emit(name, document);
|
||||
}
|
||||
getAllOpenedByClient() {
|
||||
return Array.from(this.documents.entries()).filter((doc) => this.openedInClient.has(doc[0]));
|
||||
}
|
||||
|
||||
on(name: DocumentEvent, listener: (document: Document) => void) {
|
||||
this.emitter.on(name, listener);
|
||||
}
|
||||
|
||||
private notify(name: DocumentEvent, document: Document) {
|
||||
this.emitter.emit(name, document);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +1,77 @@
|
|||
import { getFirstNonWhitespaceIndex } from './utils';
|
||||
|
||||
interface Frontmatter {
|
||||
state: null | 'open' | 'closed';
|
||||
startOffset: null | number;
|
||||
endOffset: null | number;
|
||||
state: null | 'open' | 'closed';
|
||||
startOffset: null | number;
|
||||
endOffset: null | number;
|
||||
}
|
||||
|
||||
interface Content {
|
||||
firstNonWhitespaceOffset: null | number;
|
||||
firstNonWhitespaceOffset: null | number;
|
||||
}
|
||||
|
||||
export interface AstroDocument {
|
||||
frontmatter: Frontmatter
|
||||
content: Content;
|
||||
frontmatter: Frontmatter;
|
||||
content: Content;
|
||||
}
|
||||
|
||||
/** Parses a document to collect metadata about Astro features */
|
||||
export function parseAstro(content: string): AstroDocument {
|
||||
const frontmatter = getFrontmatter(content)
|
||||
return {
|
||||
frontmatter,
|
||||
content: getContent(content, frontmatter)
|
||||
}
|
||||
const frontmatter = getFrontmatter(content);
|
||||
return {
|
||||
frontmatter,
|
||||
content: getContent(content, frontmatter),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get frontmatter metadata */
|
||||
function getFrontmatter(content: string): Frontmatter {
|
||||
/** Quickly check how many `---` blocks are in the document */
|
||||
function getFrontmatterState(): Frontmatter['state'] {
|
||||
const parts = content.trim().split('---').length;
|
||||
switch (parts) {
|
||||
case 1: return null;
|
||||
case 2: return 'open';
|
||||
default: return 'closed';
|
||||
}
|
||||
/** Quickly check how many `---` blocks are in the document */
|
||||
function getFrontmatterState(): Frontmatter['state'] {
|
||||
const parts = content.trim().split('---').length;
|
||||
switch (parts) {
|
||||
case 1:
|
||||
return null;
|
||||
case 2:
|
||||
return 'open';
|
||||
default:
|
||||
return 'closed';
|
||||
}
|
||||
const state = getFrontmatterState();
|
||||
}
|
||||
const state = getFrontmatterState();
|
||||
|
||||
/** Construct a range containing the document's frontmatter */
|
||||
function getFrontmatterOffsets(): [number|null, number|null] {
|
||||
const startOffset = content.indexOf('---');
|
||||
if (startOffset === -1) return [null, null];
|
||||
const endOffset = content.slice(startOffset + 3).indexOf('---') + 3;
|
||||
if (endOffset === -1) return [startOffset, null];
|
||||
return [startOffset, endOffset];
|
||||
}
|
||||
const [startOffset, endOffset] = getFrontmatterOffsets();
|
||||
/** Construct a range containing the document's frontmatter */
|
||||
function getFrontmatterOffsets(): [number | null, number | null] {
|
||||
const startOffset = content.indexOf('---');
|
||||
if (startOffset === -1) return [null, null];
|
||||
const endOffset = content.slice(startOffset + 3).indexOf('---') + 3;
|
||||
if (endOffset === -1) return [startOffset, null];
|
||||
return [startOffset, endOffset];
|
||||
}
|
||||
const [startOffset, endOffset] = getFrontmatterOffsets();
|
||||
|
||||
return {
|
||||
state,
|
||||
startOffset,
|
||||
endOffset
|
||||
};
|
||||
return {
|
||||
state,
|
||||
startOffset,
|
||||
endOffset,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get content metadata */
|
||||
function getContent(content: string, frontmatter: Frontmatter): Content {
|
||||
switch (frontmatter.state) {
|
||||
case null: {
|
||||
const offset = getFirstNonWhitespaceIndex(content);
|
||||
return { firstNonWhitespaceOffset: offset === -1 ? null : offset }
|
||||
}
|
||||
case 'open': {
|
||||
return { firstNonWhitespaceOffset: null }
|
||||
}
|
||||
case 'closed': {
|
||||
const { endOffset } = frontmatter;
|
||||
const end = (endOffset ?? 0) + 3;
|
||||
const offset = getFirstNonWhitespaceIndex(content.slice(end))
|
||||
return { firstNonWhitespaceOffset: end + offset }
|
||||
}
|
||||
switch (frontmatter.state) {
|
||||
case null: {
|
||||
const offset = getFirstNonWhitespaceIndex(content);
|
||||
return { firstNonWhitespaceOffset: offset === -1 ? null : offset };
|
||||
}
|
||||
case 'open': {
|
||||
return { firstNonWhitespaceOffset: null };
|
||||
}
|
||||
case 'closed': {
|
||||
const { endOffset } = frontmatter;
|
||||
const end = (endOffset ?? 0) + 3;
|
||||
const offset = getFirstNonWhitespaceIndex(content.slice(end));
|
||||
return { firstNonWhitespaceOffset: end + offset };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
getLanguageService,
|
||||
HTMLDocument,
|
||||
TokenType,
|
||||
ScannerState,
|
||||
Scanner,
|
||||
Node,
|
||||
Position
|
||||
} from 'vscode-html-languageservice';
|
||||
import { getLanguageService, HTMLDocument, TokenType, ScannerState, Scanner, Node, Position } from 'vscode-html-languageservice';
|
||||
import { Document } from './Document';
|
||||
import { isInsideExpression } from './utils';
|
||||
|
||||
|
@ -16,154 +8,134 @@ const parser = getLanguageService();
|
|||
* Parses text as HTML
|
||||
*/
|
||||
export function parseHtml(text: string): HTMLDocument {
|
||||
const preprocessed = preprocess(text);
|
||||
const preprocessed = preprocess(text);
|
||||
|
||||
// We can safely only set getText because only this is used for parsing
|
||||
const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });
|
||||
// We can safely only set getText because only this is used for parsing
|
||||
const parsedDoc = parser.parseHTMLDocument(<any>{ getText: () => preprocessed });
|
||||
|
||||
return parsedDoc;
|
||||
return parsedDoc;
|
||||
}
|
||||
|
||||
const createScanner = parser.createScanner as (
|
||||
input: string,
|
||||
initialOffset?: number,
|
||||
initialState?: ScannerState
|
||||
) => Scanner;
|
||||
const createScanner = parser.createScanner as (input: string, initialOffset?: number, initialState?: ScannerState) => Scanner;
|
||||
|
||||
/**
|
||||
* scan the text and remove any `>` or `<` that cause the tag to end short,
|
||||
*/
|
||||
function preprocess(text: string) {
|
||||
let scanner = createScanner(text);
|
||||
let token = scanner.scan();
|
||||
let currentStartTagStart: number | null = null;
|
||||
let scanner = createScanner(text);
|
||||
let token = scanner.scan();
|
||||
let currentStartTagStart: number | null = null;
|
||||
|
||||
while (token !== TokenType.EOS) {
|
||||
const offset = scanner.getTokenOffset();
|
||||
while (token !== TokenType.EOS) {
|
||||
const offset = scanner.getTokenOffset();
|
||||
|
||||
if (token === TokenType.StartTagOpen) {
|
||||
currentStartTagStart = offset;
|
||||
}
|
||||
|
||||
if (token === TokenType.StartTagClose) {
|
||||
if (shouldBlankStartOrEndTagLike(offset)) {
|
||||
blankStartOrEndTagLike(offset);
|
||||
} else {
|
||||
currentStartTagStart = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (token === TokenType.StartTagSelfClose) {
|
||||
currentStartTagStart = null;
|
||||
}
|
||||
|
||||
// <Foo checked={a < 1}>
|
||||
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
|
||||
if (
|
||||
token === TokenType.Unknown &&
|
||||
scanner.getScannerState() === ScannerState.WithinTag &&
|
||||
scanner.getTokenText() === '<' &&
|
||||
shouldBlankStartOrEndTagLike(offset)
|
||||
) {
|
||||
blankStartOrEndTagLike(offset);
|
||||
}
|
||||
|
||||
token = scanner.scan();
|
||||
if (token === TokenType.StartTagOpen) {
|
||||
currentStartTagStart = offset;
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
function shouldBlankStartOrEndTagLike(offset: number) {
|
||||
// not null rather than falsy, otherwise it won't work on first tag(0)
|
||||
return (
|
||||
currentStartTagStart !== null &&
|
||||
isInsideExpression(text, currentStartTagStart, offset)
|
||||
);
|
||||
if (token === TokenType.StartTagClose) {
|
||||
if (shouldBlankStartOrEndTagLike(offset)) {
|
||||
blankStartOrEndTagLike(offset);
|
||||
} else {
|
||||
currentStartTagStart = null;
|
||||
}
|
||||
}
|
||||
|
||||
function blankStartOrEndTagLike(offset: number) {
|
||||
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
|
||||
scanner = createScanner(text, offset, ScannerState.WithinTag);
|
||||
if (token === TokenType.StartTagSelfClose) {
|
||||
currentStartTagStart = null;
|
||||
}
|
||||
|
||||
// <Foo checked={a < 1}>
|
||||
// https://github.com/microsoft/vscode-html-languageservice/blob/71806ef57be07e1068ee40900ef8b0899c80e68a/src/parser/htmlScanner.ts#L327
|
||||
if (token === TokenType.Unknown && scanner.getScannerState() === ScannerState.WithinTag && scanner.getTokenText() === '<' && shouldBlankStartOrEndTagLike(offset)) {
|
||||
blankStartOrEndTagLike(offset);
|
||||
}
|
||||
|
||||
token = scanner.scan();
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
function shouldBlankStartOrEndTagLike(offset: number) {
|
||||
// not null rather than falsy, otherwise it won't work on first tag(0)
|
||||
return currentStartTagStart !== null && isInsideExpression(text, currentStartTagStart, offset);
|
||||
}
|
||||
|
||||
function blankStartOrEndTagLike(offset: number) {
|
||||
text = text.substring(0, offset) + ' ' + text.substring(offset + 1);
|
||||
scanner = createScanner(text, offset, ScannerState.WithinTag);
|
||||
}
|
||||
}
|
||||
|
||||
export interface AttributeContext {
|
||||
name: string;
|
||||
inValue: boolean;
|
||||
valueRange?: [number, number];
|
||||
name: string;
|
||||
inValue: boolean;
|
||||
valueRange?: [number, number];
|
||||
}
|
||||
|
||||
export function getAttributeContextAtPosition(
|
||||
document: Document,
|
||||
position: Position
|
||||
): AttributeContext | null {
|
||||
const offset = document.offsetAt(position);
|
||||
const { html } = document;
|
||||
const tag = html.findNodeAt(offset);
|
||||
|
||||
if (!inStartTag(offset, tag) || !tag.attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = document.getText();
|
||||
const beforeStartTagEnd =
|
||||
text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd));
|
||||
|
||||
const scanner = createScanner(beforeStartTagEnd, tag.start);
|
||||
|
||||
let token = scanner.scan();
|
||||
let currentAttributeName: string | undefined;
|
||||
const inTokenRange = () =>
|
||||
scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
|
||||
while (token != TokenType.EOS) {
|
||||
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
|
||||
if (token === TokenType.AttributeName) {
|
||||
currentAttributeName = scanner.getTokenText();
|
||||
|
||||
if (inTokenRange()) {
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: false
|
||||
};
|
||||
}
|
||||
} else if (token === TokenType.DelimiterAssign) {
|
||||
if (scanner.getTokenEnd() === offset && currentAttributeName) {
|
||||
const nextToken = scanner.scan();
|
||||
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: true,
|
||||
valueRange: [
|
||||
offset,
|
||||
nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset
|
||||
]
|
||||
};
|
||||
}
|
||||
} else if (token === TokenType.AttributeValue) {
|
||||
if (inTokenRange() && currentAttributeName) {
|
||||
let start = scanner.getTokenOffset();
|
||||
let end = scanner.getTokenEnd();
|
||||
const char = text[start];
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: true,
|
||||
valueRange: [start, end]
|
||||
};
|
||||
}
|
||||
currentAttributeName = undefined;
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
export function getAttributeContextAtPosition(document: Document, position: Position): AttributeContext | null {
|
||||
const offset = document.offsetAt(position);
|
||||
const { html } = document;
|
||||
const tag = html.findNodeAt(offset);
|
||||
|
||||
if (!inStartTag(offset, tag) || !tag.attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = document.getText();
|
||||
const beforeStartTagEnd = text.substring(0, tag.start) + preprocess(text.substring(tag.start, tag.startTagEnd));
|
||||
|
||||
const scanner = createScanner(beforeStartTagEnd, tag.start);
|
||||
|
||||
let token = scanner.scan();
|
||||
let currentAttributeName: string | undefined;
|
||||
const inTokenRange = () => scanner.getTokenOffset() <= offset && offset <= scanner.getTokenEnd();
|
||||
while (token != TokenType.EOS) {
|
||||
// adopted from https://github.com/microsoft/vscode-html-languageservice/blob/2f7ae4df298ac2c299a40e9024d118f4a9dc0c68/src/services/htmlCompletion.ts#L402
|
||||
if (token === TokenType.AttributeName) {
|
||||
currentAttributeName = scanner.getTokenText();
|
||||
|
||||
if (inTokenRange()) {
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: false,
|
||||
};
|
||||
}
|
||||
} else if (token === TokenType.DelimiterAssign) {
|
||||
if (scanner.getTokenEnd() === offset && currentAttributeName) {
|
||||
const nextToken = scanner.scan();
|
||||
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: true,
|
||||
valueRange: [offset, nextToken === TokenType.AttributeValue ? scanner.getTokenEnd() : offset],
|
||||
};
|
||||
}
|
||||
} else if (token === TokenType.AttributeValue) {
|
||||
if (inTokenRange() && currentAttributeName) {
|
||||
let start = scanner.getTokenOffset();
|
||||
let end = scanner.getTokenEnd();
|
||||
const char = text[start];
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
|
||||
return {
|
||||
name: currentAttributeName,
|
||||
inValue: true,
|
||||
valueRange: [start, end],
|
||||
};
|
||||
}
|
||||
currentAttributeName = undefined;
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function inStartTag(offset: number, node: Node) {
|
||||
return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
|
||||
return offset > node.start && node.startTagEnd != undefined && offset < node.startTagEnd;
|
||||
}
|
||||
|
|
|
@ -5,63 +5,52 @@ import { clamp } from '../../utils';
|
|||
* Gets word range at position.
|
||||
* Delimiter is by default a whitespace, but can be adjusted.
|
||||
*/
|
||||
export function getWordRangeAt(
|
||||
str: string,
|
||||
pos: number,
|
||||
delimiterRegex = { left: /\S+$/, right: /\s/ }
|
||||
): { start: number; end: number } {
|
||||
let start = str.slice(0, pos).search(delimiterRegex.left);
|
||||
if (start < 0) {
|
||||
start = pos;
|
||||
}
|
||||
export function getWordRangeAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): { start: number; end: number } {
|
||||
let start = str.slice(0, pos).search(delimiterRegex.left);
|
||||
if (start < 0) {
|
||||
start = pos;
|
||||
}
|
||||
|
||||
let end = str.slice(pos).search(delimiterRegex.right);
|
||||
if (end < 0) {
|
||||
end = str.length;
|
||||
} else {
|
||||
end = end + pos;
|
||||
}
|
||||
let end = str.slice(pos).search(delimiterRegex.right);
|
||||
if (end < 0) {
|
||||
end = str.length;
|
||||
} else {
|
||||
end = end + pos;
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets word at position.
|
||||
* Delimiter is by default a whitespace, but can be adjusted.
|
||||
*/
|
||||
export function getWordAt(
|
||||
str: string,
|
||||
pos: number,
|
||||
delimiterRegex = { left: /\S+$/, right: /\s/ }
|
||||
): string {
|
||||
const { start, end } = getWordRangeAt(str, pos, delimiterRegex);
|
||||
return str.slice(start, end);
|
||||
export function getWordAt(str: string, pos: number, delimiterRegex = { left: /\S+$/, right: /\s/ }): string {
|
||||
const { start, end } = getWordRangeAt(str, pos, delimiterRegex);
|
||||
return str.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets index of first-non-whitespace character.
|
||||
*/
|
||||
export function getFirstNonWhitespaceIndex(str: string): number {
|
||||
return str.length - str.trimStart().length;
|
||||
return str.length - str.trimStart().length;
|
||||
}
|
||||
|
||||
/** checks if a position is currently inside of an expression */
|
||||
export function isInsideExpression(html: string, tagStart: number, position: number) {
|
||||
const charactersInNode = html.substring(tagStart, position);
|
||||
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
|
||||
const charactersInNode = html.substring(tagStart, position);
|
||||
return charactersInNode.lastIndexOf('{') > charactersInNode.lastIndexOf('}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a given offset is inside of the document frontmatter
|
||||
* Returns if a given offset is inside of the document frontmatter
|
||||
*/
|
||||
export function isInsideFrontmatter(
|
||||
text: string,
|
||||
offset: number
|
||||
): boolean {
|
||||
let start = text.slice(0, offset).trim().split('---').length;
|
||||
let end = text.slice(offset).trim().split('---').length;
|
||||
export function isInsideFrontmatter(text: string, offset: number): boolean {
|
||||
let start = text.slice(0, offset).trim().split('---').length;
|
||||
let end = text.slice(offset).trim().split('---').length;
|
||||
|
||||
return start > 1 && start < 3 && end >= 1;
|
||||
return start > 1 && start < 3 && end >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,28 +59,28 @@ export function isInsideFrontmatter(
|
|||
* @param text The text for which the position should be retrived
|
||||
*/
|
||||
export function positionAt(offset: number, text: string): Position {
|
||||
offset = clamp(offset, 0, text.length);
|
||||
offset = clamp(offset, 0, text.length);
|
||||
|
||||
const lineOffsets = getLineOffsets(text);
|
||||
let low = 0;
|
||||
let high = lineOffsets.length;
|
||||
if (high === 0) {
|
||||
return Position.create(0, offset);
|
||||
const lineOffsets = getLineOffsets(text);
|
||||
let low = 0;
|
||||
let high = lineOffsets.length;
|
||||
if (high === 0) {
|
||||
return Position.create(0, offset);
|
||||
}
|
||||
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (lineOffsets[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (lineOffsets[mid] > offset) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// low is the least x for which the line offset is larger than the current offset
|
||||
// or array.length if no line offset is larger than the current offset
|
||||
const line = low - 1;
|
||||
return Position.create(line, offset - lineOffsets[line]);
|
||||
// low is the least x for which the line offset is larger than the current offset
|
||||
// or array.length if no line offset is larger than the current offset
|
||||
const line = low - 1;
|
||||
return Position.create(line, offset - lineOffsets[line]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,40 +89,39 @@ export function positionAt(offset: number, text: string): Position {
|
|||
* @param text The text for which the offset should be retrived
|
||||
*/
|
||||
export function offsetAt(position: Position, text: string): number {
|
||||
const lineOffsets = getLineOffsets(text);
|
||||
const lineOffsets = getLineOffsets(text);
|
||||
|
||||
if (position.line >= lineOffsets.length) {
|
||||
return text.length;
|
||||
} else if (position.line < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (position.line >= lineOffsets.length) {
|
||||
return text.length;
|
||||
} else if (position.line < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lineOffset = lineOffsets[position.line];
|
||||
const nextLineOffset =
|
||||
position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
|
||||
const lineOffset = lineOffsets[position.line];
|
||||
const nextLineOffset = position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : text.length;
|
||||
|
||||
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
|
||||
return clamp(nextLineOffset, lineOffset, lineOffset + position.character);
|
||||
}
|
||||
|
||||
function getLineOffsets(text: string) {
|
||||
const lineOffsets = [];
|
||||
let isLineStart = true;
|
||||
const lineOffsets = [];
|
||||
let isLineStart = true;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isLineStart) {
|
||||
lineOffsets.push(i);
|
||||
isLineStart = false;
|
||||
}
|
||||
const ch = text.charAt(i);
|
||||
isLineStart = ch === '\r' || ch === '\n';
|
||||
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isLineStart) {
|
||||
lineOffsets.push(i);
|
||||
isLineStart = false;
|
||||
}
|
||||
|
||||
if (isLineStart && text.length > 0) {
|
||||
lineOffsets.push(text.length);
|
||||
const ch = text.charAt(i);
|
||||
isLineStart = ch === '\r' || ch === '\n';
|
||||
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return lineOffsets;
|
||||
if (isLineStart && text.length > 0) {
|
||||
lineOffsets.push(text.length);
|
||||
}
|
||||
|
||||
return lineOffsets;
|
||||
}
|
||||
|
|
|
@ -71,10 +71,12 @@ export function startServer() {
|
|||
connection.onDidChangeTextDocument((evt) => docManager.updateDocument(evt.textDocument.uri, evt.contentChanges));
|
||||
|
||||
connection.onDidChangeWatchedFiles((evt) => {
|
||||
const params = evt.changes.map(change => ({
|
||||
fileName: urlToPath(change.uri),
|
||||
changeType: change.type
|
||||
})).filter(change => !!change.fileName)
|
||||
const params = evt.changes
|
||||
.map((change) => ({
|
||||
fileName: urlToPath(change.uri),
|
||||
changeType: change.type,
|
||||
}))
|
||||
.filter((change) => !!change.fileName);
|
||||
|
||||
pluginHost.onWatchFileChanges(params);
|
||||
});
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
|
||||
import {
|
||||
CompletionContext,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
Position,
|
||||
TextDocumentIdentifier,
|
||||
} from 'vscode-languageserver';
|
||||
import { CompletionContext, CompletionItem, CompletionList, Position, TextDocumentIdentifier } from 'vscode-languageserver';
|
||||
import type { DocumentManager } from '../core/documents';
|
||||
import type * as d from './interfaces';
|
||||
import { flatten } from '../utils';
|
||||
|
@ -13,154 +6,107 @@ import { FoldingRange } from 'vscode-languageserver-types';
|
|||
|
||||
// eslint-disable-next-line no-shadow
|
||||
enum ExecuteMode {
|
||||
None,
|
||||
FirstNonNull,
|
||||
Collect
|
||||
None,
|
||||
FirstNonNull,
|
||||
Collect,
|
||||
}
|
||||
|
||||
export class PluginHost {
|
||||
private plugins: d.Plugin[] = [];
|
||||
private plugins: d.Plugin[] = [];
|
||||
|
||||
constructor(private documentsManager: DocumentManager) {}
|
||||
constructor(private documentsManager: DocumentManager) {}
|
||||
|
||||
register(plugin: d.Plugin) {
|
||||
this.plugins.push(plugin);
|
||||
register(plugin: d.Plugin) {
|
||||
this.plugins.push(plugin);
|
||||
}
|
||||
|
||||
async getCompletions(textDocument: TextDocumentIdentifier, position: Position, completionContext?: CompletionContext): Promise<CompletionList> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
async getCompletions(
|
||||
textDocument: TextDocumentIdentifier,
|
||||
position: Position,
|
||||
completionContext?: CompletionContext
|
||||
): Promise<CompletionList> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
const completions = (await this.execute<CompletionList>('getCompletions', [document, position, completionContext], ExecuteMode.Collect)).filter(
|
||||
(completion) => completion != null
|
||||
);
|
||||
|
||||
let flattenedCompletions = flatten(completions.map((completion) => completion.items));
|
||||
const isIncomplete = completions.reduce((incomplete, completion) => incomplete || completion.isIncomplete, false as boolean);
|
||||
|
||||
return CompletionList.create(flattenedCompletions, isIncomplete);
|
||||
}
|
||||
|
||||
async resolveCompletion(textDocument: TextDocumentIdentifier, completionItem: d.AppCompletionItem): Promise<CompletionItem> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
const result = await this.execute<CompletionItem>('resolveCompletion', [document, completionItem], ExecuteMode.FirstNonNull);
|
||||
|
||||
return result ?? completionItem;
|
||||
}
|
||||
|
||||
async doTagComplete(textDocument: TextDocumentIdentifier, position: Position): Promise<string | null> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
return this.execute<string | null>('doTagComplete', [document, position], ExecuteMode.FirstNonNull);
|
||||
}
|
||||
|
||||
async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise<FoldingRange[] | null> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
const foldingRanges = flatten(await this.execute<FoldingRange[]>('getFoldingRanges', [document], ExecuteMode.Collect)).filter((completion) => completion != null);
|
||||
|
||||
return foldingRanges;
|
||||
}
|
||||
|
||||
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
|
||||
for (const support of this.plugins) {
|
||||
support.onWatchFileChanges?.(onWatchFileChangesParams);
|
||||
}
|
||||
}
|
||||
|
||||
private getDocument(uri: string) {
|
||||
return this.documentsManager.get(uri);
|
||||
}
|
||||
|
||||
private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.FirstNonNull): Promise<T | null>;
|
||||
private execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.Collect): Promise<T[]>;
|
||||
private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>;
|
||||
private async execute<T>(name: keyof d.LSProvider, args: any[], mode: ExecuteMode): Promise<(T | null) | T[] | void> {
|
||||
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
|
||||
|
||||
switch (mode) {
|
||||
case ExecuteMode.FirstNonNull:
|
||||
for (const plugin of plugins) {
|
||||
const res = await this.tryExecutePlugin(plugin, name, args, null);
|
||||
if (res != null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const completions = (
|
||||
await this.execute<CompletionList>(
|
||||
'getCompletions',
|
||||
[document, position, completionContext],
|
||||
ExecuteMode.Collect
|
||||
)
|
||||
).filter((completion) => completion != null);
|
||||
|
||||
let flattenedCompletions = flatten(completions.map((completion) => completion.items));
|
||||
const isIncomplete = completions.reduce(
|
||||
(incomplete, completion) => incomplete || completion.isIncomplete,
|
||||
false as boolean
|
||||
);
|
||||
|
||||
return CompletionList.create(flattenedCompletions, isIncomplete);
|
||||
return null;
|
||||
case ExecuteMode.Collect:
|
||||
return Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])));
|
||||
case ExecuteMode.None:
|
||||
await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveCompletion(
|
||||
textDocument: TextDocumentIdentifier,
|
||||
completionItem: d.AppCompletionItem
|
||||
): Promise<CompletionItem> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
const result = await this.execute<CompletionItem>(
|
||||
'resolveCompletion',
|
||||
[document, completionItem],
|
||||
ExecuteMode.FirstNonNull
|
||||
);
|
||||
|
||||
return result ?? completionItem;
|
||||
}
|
||||
|
||||
async doTagComplete(
|
||||
textDocument: TextDocumentIdentifier,
|
||||
position: Position
|
||||
): Promise<string | null> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
return this.execute<string | null>(
|
||||
'doTagComplete',
|
||||
[document, position],
|
||||
ExecuteMode.FirstNonNull
|
||||
);
|
||||
}
|
||||
|
||||
async getFoldingRanges(
|
||||
textDocument: TextDocumentIdentifier
|
||||
): Promise<FoldingRange[]|null> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
const foldingRanges = flatten(await this.execute<FoldingRange[]>(
|
||||
'getFoldingRanges',
|
||||
[document],
|
||||
ExecuteMode.Collect
|
||||
)).filter((completion) => completion != null)
|
||||
|
||||
return foldingRanges;
|
||||
}
|
||||
|
||||
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
|
||||
for (const support of this.plugins) {
|
||||
support.onWatchFileChanges?.(onWatchFileChangesParams);
|
||||
}
|
||||
}
|
||||
|
||||
private getDocument(uri: string) {
|
||||
return this.documentsManager.get(uri);
|
||||
}
|
||||
|
||||
private execute<T>(
|
||||
name: keyof d.LSProvider,
|
||||
args: any[],
|
||||
mode: ExecuteMode.FirstNonNull
|
||||
): Promise<T | null>;
|
||||
private execute<T>(
|
||||
name: keyof d.LSProvider,
|
||||
args: any[],
|
||||
mode: ExecuteMode.Collect
|
||||
): Promise<T[]>;
|
||||
private execute(name: keyof d.LSProvider, args: any[], mode: ExecuteMode.None): Promise<void>;
|
||||
private async execute<T>(
|
||||
name: keyof d.LSProvider,
|
||||
args: any[],
|
||||
mode: ExecuteMode
|
||||
): Promise<(T | null) | T[] | void> {
|
||||
const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
|
||||
|
||||
switch (mode) {
|
||||
case ExecuteMode.FirstNonNull:
|
||||
for (const plugin of plugins) {
|
||||
const res = await this.tryExecutePlugin(plugin, name, args, null);
|
||||
if (res != null) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case ExecuteMode.Collect:
|
||||
return Promise.all(
|
||||
plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, []))
|
||||
);
|
||||
case ExecuteMode.None:
|
||||
await Promise.all(
|
||||
plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null))
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) {
|
||||
try {
|
||||
return await plugin[fnName](...args);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return failValue;
|
||||
}
|
||||
private async tryExecutePlugin(plugin: any, fnName: string, args: any[], failValue: any) {
|
||||
try {
|
||||
return await plugin[fnName](...args);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return failValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
|
|||
endLine: end.line,
|
||||
endCharacter: end.character,
|
||||
kind: FoldingRangeKind.Imports,
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -34,15 +34,7 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
|
|||
isIncomplete: true,
|
||||
items: [],
|
||||
};
|
||||
this.lang.setCompletionParticipants([
|
||||
getEmmetCompletionParticipants(
|
||||
document,
|
||||
position,
|
||||
'html',
|
||||
this.configManager.getEmmetConfig(),
|
||||
emmetResults
|
||||
)
|
||||
]);
|
||||
this.lang.setCompletionParticipants([getEmmetCompletionParticipants(document, position, 'html', this.configManager.getEmmetConfig(), emmetResults)]);
|
||||
|
||||
const results = this.lang.doComplete(document, position, html);
|
||||
const items = this.toCompletionItems(results.items);
|
||||
|
@ -54,14 +46,13 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
|
|||
);
|
||||
}
|
||||
|
||||
getFoldingRanges(document: Document): FoldingRange[]|null {
|
||||
getFoldingRanges(document: Document): FoldingRange[] | null {
|
||||
const html = this.documents.get(document);
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.lang.getFoldingRanges(document);
|
||||
|
||||
}
|
||||
|
||||
doTagComplete(document: Document, position: Position): string | null {
|
||||
|
|
|
@ -1,217 +1,167 @@
|
|||
import { CompletionContext, FileChangeType, LinkedEditingRanges, SemanticTokens, SignatureHelpContext, TextDocumentContentChangeEvent } from 'vscode-languageserver';
|
||||
import {
|
||||
CompletionContext,
|
||||
FileChangeType,
|
||||
LinkedEditingRanges,
|
||||
SemanticTokens,
|
||||
SignatureHelpContext,
|
||||
TextDocumentContentChangeEvent
|
||||
} from 'vscode-languageserver';
|
||||
import {
|
||||
CodeAction,
|
||||
CodeActionContext,
|
||||
Color,
|
||||
ColorInformation,
|
||||
ColorPresentation,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
DefinitionLink,
|
||||
Diagnostic,
|
||||
FormattingOptions,
|
||||
Hover,
|
||||
Location,
|
||||
Position,
|
||||
Range,
|
||||
ReferenceContext,
|
||||
SymbolInformation,
|
||||
TextDocumentIdentifier,
|
||||
TextEdit,
|
||||
WorkspaceEdit,
|
||||
SelectionRange,
|
||||
SignatureHelp,
|
||||
FoldingRange
|
||||
CodeAction,
|
||||
CodeActionContext,
|
||||
Color,
|
||||
ColorInformation,
|
||||
ColorPresentation,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
DefinitionLink,
|
||||
Diagnostic,
|
||||
FormattingOptions,
|
||||
Hover,
|
||||
Location,
|
||||
Position,
|
||||
Range,
|
||||
ReferenceContext,
|
||||
SymbolInformation,
|
||||
TextDocumentIdentifier,
|
||||
TextEdit,
|
||||
WorkspaceEdit,
|
||||
SelectionRange,
|
||||
SignatureHelp,
|
||||
FoldingRange,
|
||||
} from 'vscode-languageserver-types';
|
||||
import { Document } from '../core/documents';
|
||||
|
||||
export type Resolvable<T> = T | Promise<T>;
|
||||
|
||||
export interface AppCompletionItem<T extends TextDocumentIdentifier = any> extends CompletionItem {
|
||||
data?: T;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface AppCompletionList<T extends TextDocumentIdentifier = any> extends CompletionList {
|
||||
items: Array<AppCompletionItem<T>>;
|
||||
items: Array<AppCompletionItem<T>>;
|
||||
}
|
||||
|
||||
export interface DiagnosticsProvider {
|
||||
getDiagnostics(document: Document): Resolvable<Diagnostic[]>;
|
||||
getDiagnostics(document: Document): Resolvable<Diagnostic[]>;
|
||||
}
|
||||
|
||||
export interface HoverProvider {
|
||||
doHover(document: Document, position: Position): Resolvable<Hover | null>;
|
||||
doHover(document: Document, position: Position): Resolvable<Hover | null>;
|
||||
}
|
||||
|
||||
export interface FoldingRangeProvider {
|
||||
getFoldingRanges(document: Document): Resolvable<FoldingRange[]|null>;
|
||||
getFoldingRanges(document: Document): Resolvable<FoldingRange[] | null>;
|
||||
}
|
||||
|
||||
export interface CompletionsProvider<T extends TextDocumentIdentifier = any> {
|
||||
getCompletions(
|
||||
document: Document,
|
||||
position: Position,
|
||||
completionContext?: CompletionContext
|
||||
): Resolvable<AppCompletionList<T> | null>;
|
||||
getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Resolvable<AppCompletionList<T> | null>;
|
||||
|
||||
resolveCompletion?(
|
||||
document: Document,
|
||||
completionItem: AppCompletionItem<T>
|
||||
): Resolvable<AppCompletionItem<T>>;
|
||||
resolveCompletion?(document: Document, completionItem: AppCompletionItem<T>): Resolvable<AppCompletionItem<T>>;
|
||||
}
|
||||
|
||||
export interface FormattingProvider {
|
||||
formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>;
|
||||
formatDocument(document: Document, options: FormattingOptions): Resolvable<TextEdit[]>;
|
||||
}
|
||||
|
||||
export interface TagCompleteProvider {
|
||||
doTagComplete(document: Document, position: Position): Resolvable<string | null>;
|
||||
doTagComplete(document: Document, position: Position): Resolvable<string | null>;
|
||||
}
|
||||
|
||||
export interface DocumentColorsProvider {
|
||||
getDocumentColors(document: Document): Resolvable<ColorInformation[]>;
|
||||
getDocumentColors(document: Document): Resolvable<ColorInformation[]>;
|
||||
}
|
||||
|
||||
export interface ColorPresentationsProvider {
|
||||
getColorPresentations(
|
||||
document: Document,
|
||||
range: Range,
|
||||
color: Color
|
||||
): Resolvable<ColorPresentation[]>;
|
||||
getColorPresentations(document: Document, range: Range, color: Color): Resolvable<ColorPresentation[]>;
|
||||
}
|
||||
|
||||
export interface DocumentSymbolsProvider {
|
||||
getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>;
|
||||
getDocumentSymbols(document: Document): Resolvable<SymbolInformation[]>;
|
||||
}
|
||||
|
||||
export interface DefinitionsProvider {
|
||||
getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>;
|
||||
getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[]>;
|
||||
}
|
||||
|
||||
export interface BackwardsCompatibleDefinitionsProvider {
|
||||
getDefinitions(
|
||||
document: Document,
|
||||
position: Position
|
||||
): Resolvable<DefinitionLink[] | Location[]>;
|
||||
getDefinitions(document: Document, position: Position): Resolvable<DefinitionLink[] | Location[]>;
|
||||
}
|
||||
|
||||
export interface CodeActionsProvider {
|
||||
getCodeActions(
|
||||
document: Document,
|
||||
range: Range,
|
||||
context: CodeActionContext
|
||||
): Resolvable<CodeAction[]>;
|
||||
executeCommand?(
|
||||
document: Document,
|
||||
command: string,
|
||||
args?: any[]
|
||||
): Resolvable<WorkspaceEdit | string | null>;
|
||||
getCodeActions(document: Document, range: Range, context: CodeActionContext): Resolvable<CodeAction[]>;
|
||||
executeCommand?(document: Document, command: string, args?: any[]): Resolvable<WorkspaceEdit | string | null>;
|
||||
}
|
||||
|
||||
export interface FileRename {
|
||||
oldUri: string;
|
||||
newUri: string;
|
||||
oldUri: string;
|
||||
newUri: string;
|
||||
}
|
||||
|
||||
export interface UpdateImportsProvider {
|
||||
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
|
||||
updateImports(fileRename: FileRename): Resolvable<WorkspaceEdit | null>;
|
||||
}
|
||||
|
||||
export interface RenameProvider {
|
||||
rename(
|
||||
document: Document,
|
||||
position: Position,
|
||||
newName: string
|
||||
): Resolvable<WorkspaceEdit | null>;
|
||||
prepareRename(document: Document, position: Position): Resolvable<Range | null>;
|
||||
rename(document: Document, position: Position, newName: string): Resolvable<WorkspaceEdit | null>;
|
||||
prepareRename(document: Document, position: Position): Resolvable<Range | null>;
|
||||
}
|
||||
|
||||
export interface FindReferencesProvider {
|
||||
findReferences(
|
||||
document: Document,
|
||||
position: Position,
|
||||
context: ReferenceContext
|
||||
): Promise<Location[] | null>;
|
||||
findReferences(document: Document, position: Position, context: ReferenceContext): Promise<Location[] | null>;
|
||||
}
|
||||
|
||||
export interface SignatureHelpProvider {
|
||||
getSignatureHelp(
|
||||
document: Document,
|
||||
position: Position,
|
||||
context: SignatureHelpContext | undefined
|
||||
): Resolvable<SignatureHelp | null>;
|
||||
getSignatureHelp(document: Document, position: Position, context: SignatureHelpContext | undefined): Resolvable<SignatureHelp | null>;
|
||||
}
|
||||
|
||||
export interface SelectionRangeProvider {
|
||||
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
|
||||
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
|
||||
}
|
||||
|
||||
export interface SemanticTokensProvider {
|
||||
getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>;
|
||||
getSemanticTokens(textDocument: Document, range?: Range): Resolvable<SemanticTokens | null>;
|
||||
}
|
||||
|
||||
export interface LinkedEditingRangesProvider {
|
||||
getLinkedEditingRanges(
|
||||
document: Document,
|
||||
position: Position
|
||||
): Resolvable<LinkedEditingRanges | null>;
|
||||
getLinkedEditingRanges(document: Document, position: Position): Resolvable<LinkedEditingRanges | null>;
|
||||
}
|
||||
|
||||
export interface OnWatchFileChangesPara {
|
||||
fileName: string;
|
||||
changeType: FileChangeType;
|
||||
fileName: string;
|
||||
changeType: FileChangeType;
|
||||
}
|
||||
|
||||
export interface OnWatchFileChanges {
|
||||
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
|
||||
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void;
|
||||
}
|
||||
|
||||
export interface UpdateTsOrJsFile {
|
||||
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void;
|
||||
updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void;
|
||||
}
|
||||
|
||||
type ProviderBase = DiagnosticsProvider &
|
||||
HoverProvider &
|
||||
CompletionsProvider &
|
||||
FormattingProvider &
|
||||
FoldingRangeProvider &
|
||||
TagCompleteProvider &
|
||||
DocumentColorsProvider &
|
||||
ColorPresentationsProvider &
|
||||
DocumentSymbolsProvider &
|
||||
UpdateImportsProvider &
|
||||
CodeActionsProvider &
|
||||
FindReferencesProvider &
|
||||
RenameProvider &
|
||||
SignatureHelpProvider &
|
||||
SemanticTokensProvider &
|
||||
LinkedEditingRangesProvider;
|
||||
HoverProvider &
|
||||
CompletionsProvider &
|
||||
FormattingProvider &
|
||||
FoldingRangeProvider &
|
||||
TagCompleteProvider &
|
||||
DocumentColorsProvider &
|
||||
ColorPresentationsProvider &
|
||||
DocumentSymbolsProvider &
|
||||
UpdateImportsProvider &
|
||||
CodeActionsProvider &
|
||||
FindReferencesProvider &
|
||||
RenameProvider &
|
||||
SignatureHelpProvider &
|
||||
SemanticTokensProvider &
|
||||
LinkedEditingRangesProvider;
|
||||
|
||||
export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider;
|
||||
|
||||
export interface LSPProviderConfig {
|
||||
/**
|
||||
* Whether or not completion lists that are marked as imcomplete
|
||||
* should be filtered server side.
|
||||
*/
|
||||
filterIncompleteCompletions: boolean;
|
||||
/**
|
||||
* Whether or not getDefinitions supports the LocationLink interface.
|
||||
*/
|
||||
definitionLinkSupport: boolean;
|
||||
/**
|
||||
* Whether or not completion lists that are marked as imcomplete
|
||||
* should be filtered server side.
|
||||
*/
|
||||
filterIncompleteCompletions: boolean;
|
||||
/**
|
||||
* Whether or not getDefinitions supports the LocationLink interface.
|
||||
*/
|
||||
definitionLinkSupport: boolean;
|
||||
}
|
||||
|
||||
export type Plugin = Partial<
|
||||
ProviderBase &
|
||||
DefinitionsProvider &
|
||||
OnWatchFileChanges &
|
||||
SelectionRangeProvider &
|
||||
UpdateTsOrJsFile
|
||||
>;
|
||||
export type Plugin = Partial<ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>;
|
||||
|
|
|
@ -38,7 +38,7 @@ export class LanguageServiceManager {
|
|||
const url = urlToPath(curr) as string;
|
||||
if (fileName.startsWith(url) && curr.length < url.length) return url;
|
||||
return found;
|
||||
}, '')
|
||||
}, '');
|
||||
}
|
||||
|
||||
private createDocument = (fileName: string, content: string) => {
|
||||
|
|
|
@ -6,328 +6,298 @@ import { pathToUrl } from '../../utils';
|
|||
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
|
||||
|
||||
export interface TsFilesSpec {
|
||||
include?: readonly string[];
|
||||
exclude?: readonly string[];
|
||||
include?: readonly string[];
|
||||
exclude?: readonly string[];
|
||||
}
|
||||
|
||||
export class SnapshotManager {
|
||||
private documents: Map<string, DocumentSnapshot> = new Map();
|
||||
private lastLogged = new Date(new Date().getTime() - 60_001);
|
||||
private documents: Map<string, DocumentSnapshot> = new Map();
|
||||
private lastLogged = new Date(new Date().getTime() - 60_001);
|
||||
|
||||
private readonly watchExtensions = [
|
||||
ts.Extension.Dts,
|
||||
ts.Extension.Js,
|
||||
ts.Extension.Jsx,
|
||||
ts.Extension.Ts,
|
||||
ts.Extension.Tsx,
|
||||
ts.Extension.Json
|
||||
];
|
||||
private readonly watchExtensions = [ts.Extension.Dts, ts.Extension.Js, ts.Extension.Jsx, ts.Extension.Ts, ts.Extension.Tsx, ts.Extension.Json];
|
||||
|
||||
constructor(
|
||||
private projectFiles: string[],
|
||||
private fileSpec: TsFilesSpec,
|
||||
private workspaceRoot: string
|
||||
) {
|
||||
|
||||
constructor(private projectFiles: string[], private fileSpec: TsFilesSpec, private workspaceRoot: string) {}
|
||||
|
||||
updateProjectFiles() {
|
||||
const { include, exclude } = this.fileSpec;
|
||||
|
||||
if (include?.length === 0) return;
|
||||
|
||||
const projectFiles = ts.sys.readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include);
|
||||
|
||||
this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
|
||||
}
|
||||
|
||||
updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
|
||||
const previousSnapshot = this.get(fileName);
|
||||
|
||||
if (changes) {
|
||||
if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) {
|
||||
return;
|
||||
}
|
||||
previousSnapshot.update(changes);
|
||||
} else {
|
||||
const newSnapshot = createDocumentSnapshot(fileName);
|
||||
|
||||
if (previousSnapshot) {
|
||||
newSnapshot.version = previousSnapshot.version + 1;
|
||||
} else {
|
||||
// ensure it's greater than initial version
|
||||
// so that ts server picks up the change
|
||||
newSnapshot.version += 1;
|
||||
}
|
||||
this.set(fileName, newSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
updateProjectFiles() {
|
||||
const { include, exclude } = this.fileSpec;
|
||||
|
||||
if (include?.length === 0) return;
|
||||
has(fileName: string) {
|
||||
return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
|
||||
}
|
||||
|
||||
const projectFiles = ts.sys.readDirectory(
|
||||
this.workspaceRoot,
|
||||
this.watchExtensions,
|
||||
exclude,
|
||||
include
|
||||
);
|
||||
get(fileName: string) {
|
||||
return this.documents.get(fileName);
|
||||
}
|
||||
|
||||
this.projectFiles = Array.from(new Set([...this.projectFiles, ...projectFiles]));
|
||||
}
|
||||
|
||||
updateProjectFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void {
|
||||
const previousSnapshot = this.get(fileName);
|
||||
|
||||
if (changes) {
|
||||
if (!(previousSnapshot instanceof TypeScriptDocumentSnapshot)) {
|
||||
return;
|
||||
}
|
||||
previousSnapshot.update(changes);
|
||||
} else {
|
||||
const newSnapshot = createDocumentSnapshot(fileName);
|
||||
|
||||
if (previousSnapshot) {
|
||||
newSnapshot.version = previousSnapshot.version + 1;
|
||||
} else {
|
||||
// ensure it's greater than initial version
|
||||
// so that ts server picks up the change
|
||||
newSnapshot.version += 1;
|
||||
}
|
||||
this.set(fileName, newSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
has(fileName: string) {
|
||||
return this.projectFiles.includes(fileName) || this.getFileNames().includes(fileName);
|
||||
}
|
||||
|
||||
get(fileName: string) {
|
||||
return this.documents.get(fileName);
|
||||
}
|
||||
|
||||
set(fileName: string, snapshot: DocumentSnapshot) {
|
||||
// const prev = this.get(fileName);
|
||||
this.logStatistics();
|
||||
return this.documents.set(fileName, snapshot);
|
||||
}
|
||||
|
||||
delete(fileName: string) {
|
||||
this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
|
||||
return this.documents.delete(fileName);
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
return Array.from(this.documents.keys()).map(fileName => toVirtualAstroFilePath(fileName));
|
||||
}
|
||||
|
||||
getProjectFileNames() {
|
||||
return [...this.projectFiles];
|
||||
}
|
||||
|
||||
private logStatistics() {
|
||||
const date = new Date();
|
||||
// Don't use setInterval because that will keep tests running forever
|
||||
if (date.getTime() - this.lastLogged.getTime() > 60_000) {
|
||||
this.lastLogged = date;
|
||||
|
||||
const projectFiles = this.getProjectFileNames();
|
||||
const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
|
||||
console.log(
|
||||
'SnapshotManager File Statistics:\n' +
|
||||
`Project files: ${projectFiles.length}\n` +
|
||||
`Astro files: ${
|
||||
allFiles.filter((name) => name.endsWith('.astro')).length
|
||||
}\n` +
|
||||
`From node_modules: ${
|
||||
allFiles.filter((name) => name.includes('node_modules')).length
|
||||
}\n` +
|
||||
`Total: ${allFiles.length}`
|
||||
);
|
||||
}
|
||||
set(fileName: string, snapshot: DocumentSnapshot) {
|
||||
// const prev = this.get(fileName);
|
||||
this.logStatistics();
|
||||
return this.documents.set(fileName, snapshot);
|
||||
}
|
||||
|
||||
delete(fileName: string) {
|
||||
this.projectFiles = this.projectFiles.filter((s) => s !== fileName);
|
||||
return this.documents.delete(fileName);
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
return Array.from(this.documents.keys()).map((fileName) => toVirtualAstroFilePath(fileName));
|
||||
}
|
||||
|
||||
getProjectFileNames() {
|
||||
return [...this.projectFiles];
|
||||
}
|
||||
|
||||
private logStatistics() {
|
||||
const date = new Date();
|
||||
// Don't use setInterval because that will keep tests running forever
|
||||
if (date.getTime() - this.lastLogged.getTime() > 60_000) {
|
||||
this.lastLogged = date;
|
||||
|
||||
const projectFiles = this.getProjectFileNames();
|
||||
const allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
|
||||
console.log(
|
||||
'SnapshotManager File Statistics:\n' +
|
||||
`Project files: ${projectFiles.length}\n` +
|
||||
`Astro files: ${allFiles.filter((name) => name.endsWith('.astro')).length}\n` +
|
||||
`From node_modules: ${allFiles.filter((name) => name.includes('node_modules')).length}\n` +
|
||||
`Total: ${allFiles.length}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DocumentSnapshot extends ts.IScriptSnapshot {
|
||||
version: number;
|
||||
filePath: string;
|
||||
scriptKind: ts.ScriptKind;
|
||||
positionAt(offset: number): Position;
|
||||
/**
|
||||
* Instantiates a source mapper.
|
||||
* `destroyFragment` needs to be called when
|
||||
* it's no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
getFragment(): Promise<DocumentFragmentSnapshot>;
|
||||
/**
|
||||
* Needs to be called when source mapper
|
||||
* is no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
destroyFragment(): void;
|
||||
/**
|
||||
* Convenience function for getText(0, getLength())
|
||||
*/
|
||||
getFullText(): string;
|
||||
version: number;
|
||||
filePath: string;
|
||||
scriptKind: ts.ScriptKind;
|
||||
positionAt(offset: number): Position;
|
||||
/**
|
||||
* Instantiates a source mapper.
|
||||
* `destroyFragment` needs to be called when
|
||||
* it's no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
getFragment(): Promise<DocumentFragmentSnapshot>;
|
||||
/**
|
||||
* Needs to be called when source mapper
|
||||
* is no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
destroyFragment(): void;
|
||||
/**
|
||||
* Convenience function for getText(0, getLength())
|
||||
*/
|
||||
getFullText(): string;
|
||||
}
|
||||
|
||||
export const createDocumentSnapshot = (filePath: string, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
|
||||
const text = ts.sys.readFile(filePath) ?? '';
|
||||
const text = ts.sys.readFile(filePath) ?? '';
|
||||
|
||||
if (isAstroFilePath(filePath)) {
|
||||
if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided');
|
||||
const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text));
|
||||
return snapshot;
|
||||
}
|
||||
if (isAstroFilePath(filePath)) {
|
||||
if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided');
|
||||
const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return new TypeScriptDocumentSnapshot(0, filePath, text);
|
||||
|
||||
}
|
||||
return new TypeScriptDocumentSnapshot(0, filePath, text);
|
||||
};
|
||||
|
||||
class AstroDocumentSnapshot implements DocumentSnapshot {
|
||||
|
||||
version = this.doc.version;
|
||||
scriptKind = ts.ScriptKind.Unknown;
|
||||
|
||||
constructor(private doc: Document) {}
|
||||
version = this.doc.version;
|
||||
scriptKind = ts.ScriptKind.Unknown;
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return new DocumentFragmentSnapshot(this.doc);
|
||||
}
|
||||
constructor(private doc: Document) {}
|
||||
|
||||
async destroyFragment() {
|
||||
return;
|
||||
}
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return new DocumentFragmentSnapshot(this.doc);
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.doc.getText();
|
||||
}
|
||||
async destroyFragment() {
|
||||
return;
|
||||
}
|
||||
|
||||
get filePath() {
|
||||
return this.doc.getFilePath() || '';
|
||||
}
|
||||
get text() {
|
||||
return this.doc.getText();
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
get filePath() {
|
||||
return this.doc.getFilePath() || '';
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
offsetAt(position: Position) {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position) {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment'|'destroyFragment'> {
|
||||
|
||||
version: number;
|
||||
filePath: string;
|
||||
url: string;
|
||||
text: string;
|
||||
class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'> {
|
||||
version: number;
|
||||
filePath: string;
|
||||
url: string;
|
||||
text: string;
|
||||
|
||||
scriptKind = ts.ScriptKind.TSX;
|
||||
scriptInfo = null;
|
||||
scriptKind = ts.ScriptKind.TSX;
|
||||
scriptInfo = null;
|
||||
|
||||
constructor(
|
||||
private doc: Document
|
||||
) {
|
||||
const filePath = doc.getFilePath();
|
||||
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
|
||||
const text = doc.getText();
|
||||
this.version = doc.version;
|
||||
this.filePath = toVirtualAstroFilePath(filePath);
|
||||
this.url = toVirtualAstroFilePath(filePath);
|
||||
this.text = this.transformContent(text);
|
||||
}
|
||||
constructor(private doc: Document) {
|
||||
const filePath = doc.getFilePath();
|
||||
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
|
||||
const text = doc.getText();
|
||||
this.version = doc.version;
|
||||
this.filePath = toVirtualAstroFilePath(filePath);
|
||||
this.url = toVirtualAstroFilePath(filePath);
|
||||
this.text = this.transformContent(text);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private transformContent(content: string) {
|
||||
return content.replace(/---/g, '///');
|
||||
}
|
||||
/** @internal */
|
||||
private transformContent(content: string) {
|
||||
return content.replace(/---/g, '///');
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
class TypeScriptDocumentSnapshot implements DocumentSnapshot {
|
||||
|
||||
scriptKind = getScriptKindFromFileName(this.filePath);
|
||||
scriptInfo = null;
|
||||
url: string;
|
||||
scriptKind = getScriptKindFromFileName(this.filePath);
|
||||
scriptInfo = null;
|
||||
url: string;
|
||||
|
||||
|
||||
constructor(public version: number, public readonly filePath: string, private text: string) {
|
||||
this.url = pathToUrl(filePath)
|
||||
constructor(public version: number, public readonly filePath: string, private text: string) {
|
||||
this.url = pathToUrl(filePath);
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return (this as unknown) as any;
|
||||
}
|
||||
|
||||
destroyFragment() {
|
||||
// nothing to clean up
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
update(changes: TextDocumentContentChangeEvent[]): void {
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = this.offsetAt(change.range.start);
|
||||
end = this.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = this.getLength();
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return this as unknown as any;
|
||||
}
|
||||
|
||||
destroyFragment() {
|
||||
// nothing to clean up
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
update(changes: TextDocumentContentChangeEvent[]): void {
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = this.offsetAt(change.range.start);
|
||||
end = this.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = this.getLength();
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
|
||||
}
|
||||
|
||||
this.version++;
|
||||
}
|
||||
this.version++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import type { Document, DocumentManager } from '../../core/documents';
|
||||
import type { ConfigManager } from '../../core/config';
|
||||
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
|
||||
import {
|
||||
CompletionContext,
|
||||
Position,
|
||||
FileChangeType
|
||||
} from 'vscode-languageserver';
|
||||
import { CompletionContext, Position, FileChangeType } from 'vscode-languageserver';
|
||||
import * as ts from 'typescript';
|
||||
import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
|
||||
import { LanguageServiceManager } from './LanguageServiceManager';
|
||||
|
@ -13,77 +9,61 @@ import { SnapshotManager } from './SnapshotManager';
|
|||
import { getScriptKindFromFileName } from './utils';
|
||||
|
||||
export class TypeScriptPlugin implements CompletionsProvider {
|
||||
private readonly docManager: DocumentManager;
|
||||
private readonly configManager: ConfigManager;
|
||||
private readonly languageServiceManager: LanguageServiceManager;
|
||||
private readonly docManager: DocumentManager;
|
||||
private readonly configManager: ConfigManager;
|
||||
private readonly languageServiceManager: LanguageServiceManager;
|
||||
|
||||
private readonly completionProvider: CompletionsProviderImpl;
|
||||
private readonly completionProvider: CompletionsProviderImpl;
|
||||
|
||||
constructor(
|
||||
docManager: DocumentManager,
|
||||
configManager: ConfigManager,
|
||||
workspaceUris: string[]
|
||||
) {
|
||||
this.docManager = docManager;
|
||||
this.configManager = configManager;
|
||||
this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris);
|
||||
|
||||
this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager);
|
||||
}
|
||||
constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
|
||||
this.docManager = docManager;
|
||||
this.configManager = configManager;
|
||||
this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris);
|
||||
|
||||
async getCompletions(
|
||||
document: Document,
|
||||
position: Position,
|
||||
completionContext?: CompletionContext
|
||||
): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
|
||||
const completions = await this.completionProvider.getCompletions(
|
||||
document,
|
||||
position,
|
||||
completionContext
|
||||
);
|
||||
this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager);
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
|
||||
const completions = await this.completionProvider.getCompletions(document, position, completionContext);
|
||||
|
||||
async resolveCompletion(
|
||||
document: Document,
|
||||
completionItem: AppCompletionItem<CompletionEntryWithIdentifer>
|
||||
): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
|
||||
return this.completionProvider.resolveCompletion(document, completionItem);
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
|
||||
const doneUpdateProjectFiles = new Set<SnapshotManager>();
|
||||
async resolveCompletion(document: Document, completionItem: AppCompletionItem<CompletionEntryWithIdentifer>): Promise<AppCompletionItem<CompletionEntryWithIdentifer>> {
|
||||
return this.completionProvider.resolveCompletion(document, completionItem);
|
||||
}
|
||||
|
||||
for (const { fileName, changeType } of onWatchFileChangesParams) {
|
||||
const scriptKind = getScriptKindFromFileName(fileName);
|
||||
async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
|
||||
const doneUpdateProjectFiles = new Set<SnapshotManager>();
|
||||
|
||||
if (scriptKind === ts.ScriptKind.Unknown) {
|
||||
// We don't deal with svelte files here
|
||||
continue;
|
||||
}
|
||||
for (const { fileName, changeType } of onWatchFileChangesParams) {
|
||||
const scriptKind = getScriptKindFromFileName(fileName);
|
||||
|
||||
const snapshotManager = await this.getSnapshotManager(fileName);
|
||||
if (changeType === FileChangeType.Created) {
|
||||
if (!doneUpdateProjectFiles.has(snapshotManager)) {
|
||||
snapshotManager.updateProjectFiles();
|
||||
doneUpdateProjectFiles.add(snapshotManager);
|
||||
}
|
||||
} else if (changeType === FileChangeType.Deleted) {
|
||||
snapshotManager.delete(fileName);
|
||||
return;
|
||||
}
|
||||
if (scriptKind === ts.ScriptKind.Unknown) {
|
||||
// We don't deal with svelte files here
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshotManager.updateProjectFile(fileName);
|
||||
const snapshotManager = await this.getSnapshotManager(fileName);
|
||||
if (changeType === FileChangeType.Created) {
|
||||
if (!doneUpdateProjectFiles.has(snapshotManager)) {
|
||||
snapshotManager.updateProjectFiles();
|
||||
doneUpdateProjectFiles.add(snapshotManager);
|
||||
}
|
||||
}
|
||||
} else if (changeType === FileChangeType.Deleted) {
|
||||
snapshotManager.delete(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getSnapshotManager(fileName: string) {
|
||||
return this.languageServiceManager.getSnapshotManager(fileName);
|
||||
snapshotManager.updateProjectFile(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public async getSnapshotManager(fileName: string) {
|
||||
return this.languageServiceManager.getSnapshotManager(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,37 +6,37 @@ import { ensureRealAstroFilePath, isAstroFilePath, isVirtualAstroFilePath, toRea
|
|||
* This should only be accessed by TS Astro module resolution.
|
||||
*/
|
||||
export function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapshot) {
|
||||
const AstroSys: ts.System = {
|
||||
...ts.sys,
|
||||
fileExists(path: string) {
|
||||
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
|
||||
console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path)));
|
||||
}
|
||||
return ts.sys.fileExists(ensureRealAstroFilePath(path));
|
||||
},
|
||||
readFile(path: string) {
|
||||
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
|
||||
console.log('readFile', path);
|
||||
}
|
||||
const snapshot = getSnapshot(path);
|
||||
return snapshot.getFullText();
|
||||
},
|
||||
readDirectory(path, extensions, exclude, include, depth) {
|
||||
const extensionsWithAstro = (extensions ?? []).concat(...['.astro']);
|
||||
const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);;
|
||||
return result;
|
||||
}
|
||||
const AstroSys: ts.System = {
|
||||
...ts.sys,
|
||||
fileExists(path: string) {
|
||||
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
|
||||
console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path)));
|
||||
}
|
||||
return ts.sys.fileExists(ensureRealAstroFilePath(path));
|
||||
},
|
||||
readFile(path: string) {
|
||||
if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
|
||||
console.log('readFile', path);
|
||||
}
|
||||
const snapshot = getSnapshot(path);
|
||||
return snapshot.getFullText();
|
||||
},
|
||||
readDirectory(path, extensions, exclude, include, depth) {
|
||||
const extensionsWithAstro = (extensions ?? []).concat(...['.astro']);
|
||||
const result = ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
if (ts.sys.realpath) {
|
||||
const realpath = ts.sys.realpath;
|
||||
AstroSys.realpath = function (path) {
|
||||
if (isVirtualAstroFilePath(path)) {
|
||||
return realpath(toRealAstroFilePath(path)) + '.ts';
|
||||
}
|
||||
return realpath(path);
|
||||
};
|
||||
}
|
||||
|
||||
if (ts.sys.realpath) {
|
||||
const realpath = ts.sys.realpath;
|
||||
AstroSys.realpath = function (path) {
|
||||
if (isVirtualAstroFilePath(path)) {
|
||||
return realpath(toRealAstroFilePath(path)) + '.ts';
|
||||
}
|
||||
return realpath(path);
|
||||
};
|
||||
}
|
||||
|
||||
return AstroSys;
|
||||
return AstroSys;
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
|
|||
data: {
|
||||
...comp,
|
||||
uri,
|
||||
position
|
||||
position,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,30 +53,22 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
return ts.sys.readDirectory(path, [...extensions, '.vue', '.svelte', '.astro', '.js', '.jsx'], exclude, include, depth);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
let configJson = (tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config) || getDefaultJsConfig();
|
||||
if (!configJson.extends) {
|
||||
configJson = Object.assign(
|
||||
{
|
||||
exclude: getDefaultExclude()
|
||||
},
|
||||
configJson
|
||||
);
|
||||
configJson = Object.assign(
|
||||
{
|
||||
exclude: getDefaultExclude(),
|
||||
},
|
||||
configJson
|
||||
);
|
||||
}
|
||||
|
||||
const project = ts.parseJsonConfigFileContent(
|
||||
configJson,
|
||||
parseConfigHost,
|
||||
workspaceRoot,
|
||||
{},
|
||||
basename(tsconfigPath),
|
||||
undefined,
|
||||
[
|
||||
{ extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
|
||||
{ extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
|
||||
{ extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }
|
||||
]
|
||||
);
|
||||
const project = ts.parseJsonConfigFileContent(configJson, parseConfigHost, workspaceRoot, {}, basename(tsconfigPath), undefined, [
|
||||
{ extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
|
||||
{ extension: '.svelte', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
|
||||
{ extension: '.astro', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred },
|
||||
]);
|
||||
|
||||
let projectVersion = 0;
|
||||
const snapshotManager = new SnapshotManager(project.fileNames, { exclude: ['node_modules', 'dist'], include: ['astro'] }, workspaceRoot || process.cwd());
|
||||
|
@ -100,15 +92,15 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
getProjectVersion: () => `${projectVersion}`,
|
||||
getScriptFileNames: () => Array.from(new Set([...snapshotManager.getFileNames(), ...snapshotManager.getProjectFileNames()])),
|
||||
getScriptSnapshot,
|
||||
getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString()
|
||||
getScriptVersion: (fileName: string) => getScriptSnapshot(fileName).version.toString(),
|
||||
};
|
||||
|
||||
const languageService = ts.createLanguageService(host);
|
||||
const languageServiceProxy = new Proxy(languageService, {
|
||||
get(target, prop) {
|
||||
return Reflect.get(target, prop);
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
tsconfigPath,
|
||||
|
@ -141,19 +133,16 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
}
|
||||
|
||||
function getScriptSnapshot(fileName: string): DocumentSnapshot {
|
||||
fileName = ensureRealAstroFilePath(fileName);
|
||||
fileName = ensureRealAstroFilePath(fileName);
|
||||
|
||||
let doc = snapshotManager.get(fileName);
|
||||
if (doc) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
doc = createDocumentSnapshot(
|
||||
fileName,
|
||||
docContext.createDocument,
|
||||
);
|
||||
snapshotManager.set(fileName, doc);
|
||||
let doc = snapshotManager.get(fileName);
|
||||
if (doc) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
doc = createDocumentSnapshot(fileName, docContext.createDocument);
|
||||
snapshotManager.set(fileName, doc);
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,7 +157,7 @@ function getDefaultJsConfig(): {
|
|||
compilerOptions: {
|
||||
maxNodeModuleJsDepth: 2,
|
||||
allowSyntheticDefaultImports: true,
|
||||
allowJs: true
|
||||
allowJs: true,
|
||||
},
|
||||
include: ['astro'],
|
||||
};
|
||||
|
|
|
@ -3,180 +3,172 @@ import { CompletionItemKind, DiagnosticSeverity } from 'vscode-languageserver';
|
|||
import { dirname } from 'path';
|
||||
import { pathToUrl } from '../../utils';
|
||||
|
||||
export function scriptElementKindToCompletionItemKind(
|
||||
kind: ts.ScriptElementKind
|
||||
): CompletionItemKind {
|
||||
switch (kind) {
|
||||
case ts.ScriptElementKind.primitiveType:
|
||||
case ts.ScriptElementKind.keyword:
|
||||
return CompletionItemKind.Keyword;
|
||||
case ts.ScriptElementKind.constElement:
|
||||
return CompletionItemKind.Constant;
|
||||
case ts.ScriptElementKind.letElement:
|
||||
case ts.ScriptElementKind.variableElement:
|
||||
case ts.ScriptElementKind.localVariableElement:
|
||||
case ts.ScriptElementKind.alias:
|
||||
return CompletionItemKind.Variable;
|
||||
case ts.ScriptElementKind.memberVariableElement:
|
||||
case ts.ScriptElementKind.memberGetAccessorElement:
|
||||
case ts.ScriptElementKind.memberSetAccessorElement:
|
||||
return CompletionItemKind.Field;
|
||||
case ts.ScriptElementKind.functionElement:
|
||||
return CompletionItemKind.Function;
|
||||
case ts.ScriptElementKind.memberFunctionElement:
|
||||
case ts.ScriptElementKind.constructSignatureElement:
|
||||
case ts.ScriptElementKind.callSignatureElement:
|
||||
case ts.ScriptElementKind.indexSignatureElement:
|
||||
return CompletionItemKind.Method;
|
||||
case ts.ScriptElementKind.enumElement:
|
||||
return CompletionItemKind.Enum;
|
||||
case ts.ScriptElementKind.moduleElement:
|
||||
case ts.ScriptElementKind.externalModuleName:
|
||||
return CompletionItemKind.Module;
|
||||
case ts.ScriptElementKind.classElement:
|
||||
case ts.ScriptElementKind.typeElement:
|
||||
return CompletionItemKind.Class;
|
||||
case ts.ScriptElementKind.interfaceElement:
|
||||
return CompletionItemKind.Interface;
|
||||
case ts.ScriptElementKind.warning:
|
||||
case ts.ScriptElementKind.scriptElement:
|
||||
return CompletionItemKind.File;
|
||||
case ts.ScriptElementKind.directory:
|
||||
return CompletionItemKind.Folder;
|
||||
case ts.ScriptElementKind.string:
|
||||
return CompletionItemKind.Constant;
|
||||
}
|
||||
return CompletionItemKind.Property;
|
||||
export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind {
|
||||
switch (kind) {
|
||||
case ts.ScriptElementKind.primitiveType:
|
||||
case ts.ScriptElementKind.keyword:
|
||||
return CompletionItemKind.Keyword;
|
||||
case ts.ScriptElementKind.constElement:
|
||||
return CompletionItemKind.Constant;
|
||||
case ts.ScriptElementKind.letElement:
|
||||
case ts.ScriptElementKind.variableElement:
|
||||
case ts.ScriptElementKind.localVariableElement:
|
||||
case ts.ScriptElementKind.alias:
|
||||
return CompletionItemKind.Variable;
|
||||
case ts.ScriptElementKind.memberVariableElement:
|
||||
case ts.ScriptElementKind.memberGetAccessorElement:
|
||||
case ts.ScriptElementKind.memberSetAccessorElement:
|
||||
return CompletionItemKind.Field;
|
||||
case ts.ScriptElementKind.functionElement:
|
||||
return CompletionItemKind.Function;
|
||||
case ts.ScriptElementKind.memberFunctionElement:
|
||||
case ts.ScriptElementKind.constructSignatureElement:
|
||||
case ts.ScriptElementKind.callSignatureElement:
|
||||
case ts.ScriptElementKind.indexSignatureElement:
|
||||
return CompletionItemKind.Method;
|
||||
case ts.ScriptElementKind.enumElement:
|
||||
return CompletionItemKind.Enum;
|
||||
case ts.ScriptElementKind.moduleElement:
|
||||
case ts.ScriptElementKind.externalModuleName:
|
||||
return CompletionItemKind.Module;
|
||||
case ts.ScriptElementKind.classElement:
|
||||
case ts.ScriptElementKind.typeElement:
|
||||
return CompletionItemKind.Class;
|
||||
case ts.ScriptElementKind.interfaceElement:
|
||||
return CompletionItemKind.Interface;
|
||||
case ts.ScriptElementKind.warning:
|
||||
case ts.ScriptElementKind.scriptElement:
|
||||
return CompletionItemKind.File;
|
||||
case ts.ScriptElementKind.directory:
|
||||
return CompletionItemKind.Folder;
|
||||
case ts.ScriptElementKind.string:
|
||||
return CompletionItemKind.Constant;
|
||||
}
|
||||
return CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
export function getCommitCharactersForScriptElement(
|
||||
kind: ts.ScriptElementKind
|
||||
): string[] | undefined {
|
||||
const commitCharacters: string[] = [];
|
||||
switch (kind) {
|
||||
case ts.ScriptElementKind.memberGetAccessorElement:
|
||||
case ts.ScriptElementKind.memberSetAccessorElement:
|
||||
case ts.ScriptElementKind.constructSignatureElement:
|
||||
case ts.ScriptElementKind.callSignatureElement:
|
||||
case ts.ScriptElementKind.indexSignatureElement:
|
||||
case ts.ScriptElementKind.enumElement:
|
||||
case ts.ScriptElementKind.interfaceElement:
|
||||
commitCharacters.push('.');
|
||||
break;
|
||||
export function getCommitCharactersForScriptElement(kind: ts.ScriptElementKind): string[] | undefined {
|
||||
const commitCharacters: string[] = [];
|
||||
switch (kind) {
|
||||
case ts.ScriptElementKind.memberGetAccessorElement:
|
||||
case ts.ScriptElementKind.memberSetAccessorElement:
|
||||
case ts.ScriptElementKind.constructSignatureElement:
|
||||
case ts.ScriptElementKind.callSignatureElement:
|
||||
case ts.ScriptElementKind.indexSignatureElement:
|
||||
case ts.ScriptElementKind.enumElement:
|
||||
case ts.ScriptElementKind.interfaceElement:
|
||||
commitCharacters.push('.');
|
||||
break;
|
||||
|
||||
case ts.ScriptElementKind.moduleElement:
|
||||
case ts.ScriptElementKind.alias:
|
||||
case ts.ScriptElementKind.constElement:
|
||||
case ts.ScriptElementKind.letElement:
|
||||
case ts.ScriptElementKind.variableElement:
|
||||
case ts.ScriptElementKind.localVariableElement:
|
||||
case ts.ScriptElementKind.memberVariableElement:
|
||||
case ts.ScriptElementKind.classElement:
|
||||
case ts.ScriptElementKind.functionElement:
|
||||
case ts.ScriptElementKind.memberFunctionElement:
|
||||
commitCharacters.push('.', ',');
|
||||
commitCharacters.push('(');
|
||||
break;
|
||||
}
|
||||
case ts.ScriptElementKind.moduleElement:
|
||||
case ts.ScriptElementKind.alias:
|
||||
case ts.ScriptElementKind.constElement:
|
||||
case ts.ScriptElementKind.letElement:
|
||||
case ts.ScriptElementKind.variableElement:
|
||||
case ts.ScriptElementKind.localVariableElement:
|
||||
case ts.ScriptElementKind.memberVariableElement:
|
||||
case ts.ScriptElementKind.classElement:
|
||||
case ts.ScriptElementKind.functionElement:
|
||||
case ts.ScriptElementKind.memberFunctionElement:
|
||||
commitCharacters.push('.', ',');
|
||||
commitCharacters.push('(');
|
||||
break;
|
||||
}
|
||||
|
||||
return commitCharacters.length === 0 ? undefined : commitCharacters;
|
||||
return commitCharacters.length === 0 ? undefined : commitCharacters;
|
||||
}
|
||||
|
||||
export function mapSeverity(category: ts.DiagnosticCategory): DiagnosticSeverity {
|
||||
switch (category) {
|
||||
case ts.DiagnosticCategory.Error:
|
||||
return DiagnosticSeverity.Error;
|
||||
case ts.DiagnosticCategory.Warning:
|
||||
return DiagnosticSeverity.Warning;
|
||||
case ts.DiagnosticCategory.Suggestion:
|
||||
return DiagnosticSeverity.Hint;
|
||||
case ts.DiagnosticCategory.Message:
|
||||
return DiagnosticSeverity.Information;
|
||||
}
|
||||
switch (category) {
|
||||
case ts.DiagnosticCategory.Error:
|
||||
return DiagnosticSeverity.Error;
|
||||
case ts.DiagnosticCategory.Warning:
|
||||
return DiagnosticSeverity.Warning;
|
||||
case ts.DiagnosticCategory.Suggestion:
|
||||
return DiagnosticSeverity.Hint;
|
||||
case ts.DiagnosticCategory.Message:
|
||||
return DiagnosticSeverity.Information;
|
||||
}
|
||||
|
||||
return DiagnosticSeverity.Error;
|
||||
return DiagnosticSeverity.Error;
|
||||
}
|
||||
|
||||
export function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
|
||||
const ext = fileName.substr(fileName.lastIndexOf('.'));
|
||||
switch (ext.toLowerCase()) {
|
||||
case ts.Extension.Js:
|
||||
return ts.ScriptKind.JS;
|
||||
case ts.Extension.Jsx:
|
||||
return ts.ScriptKind.JSX;
|
||||
case ts.Extension.Ts:
|
||||
return ts.ScriptKind.TS;
|
||||
case ts.Extension.Tsx:
|
||||
return ts.ScriptKind.TSX;
|
||||
case ts.Extension.Json:
|
||||
return ts.ScriptKind.JSON;
|
||||
default:
|
||||
return ts.ScriptKind.Unknown;
|
||||
}
|
||||
const ext = fileName.substr(fileName.lastIndexOf('.'));
|
||||
switch (ext.toLowerCase()) {
|
||||
case ts.Extension.Js:
|
||||
return ts.ScriptKind.JS;
|
||||
case ts.Extension.Jsx:
|
||||
return ts.ScriptKind.JSX;
|
||||
case ts.Extension.Ts:
|
||||
return ts.ScriptKind.TS;
|
||||
case ts.Extension.Tsx:
|
||||
return ts.ScriptKind.TSX;
|
||||
case ts.Extension.Json:
|
||||
return ts.ScriptKind.JSON;
|
||||
default:
|
||||
return ts.ScriptKind.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAstroFilePath(filePath: string) {
|
||||
return filePath.endsWith('.astro');
|
||||
return filePath.endsWith('.astro');
|
||||
}
|
||||
|
||||
export function isVirtualAstroFilePath(filePath: string) {
|
||||
return filePath.endsWith('.astro.ts');
|
||||
return filePath.endsWith('.astro.ts');
|
||||
}
|
||||
|
||||
export function toVirtualAstroFilePath(filePath: string) {
|
||||
return `${filePath}.ts`;
|
||||
return `${filePath}.ts`;
|
||||
}
|
||||
|
||||
export function toRealAstroFilePath(filePath: string) {
|
||||
return filePath.slice(0, -'.ts'.length);
|
||||
return filePath.slice(0, -'.ts'.length);
|
||||
}
|
||||
|
||||
export function ensureRealAstroFilePath(filePath: string) {
|
||||
return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath;
|
||||
return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath;
|
||||
}
|
||||
|
||||
export function findTsConfigPath(fileName: string, rootUris: string[]) {
|
||||
const searchDir = dirname(fileName);
|
||||
const path =
|
||||
ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') ||
|
||||
ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') ||
|
||||
'';
|
||||
// Don't return config files that exceed the current workspace context.
|
||||
return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
|
||||
const searchDir = dirname(fileName);
|
||||
const path = ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || '';
|
||||
// Don't return config files that exceed the current workspace context.
|
||||
return !!path && rootUris.some((rootUri) => isSubPath(rootUri, path)) ? path : '';
|
||||
}
|
||||
|
||||
/** */
|
||||
export function isSubPath(uri: string, possibleSubPath: string): boolean {
|
||||
return pathToUrl(possibleSubPath).startsWith(uri);
|
||||
return pathToUrl(possibleSubPath).startsWith(uri);
|
||||
}
|
||||
|
||||
|
||||
/** Substitutes */
|
||||
export function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
|
||||
let accumulatedWS = 0;
|
||||
result += before;
|
||||
for (let i = start + before.length; i < end; i++) {
|
||||
let ch = oldContent[i];
|
||||
if (ch === '\n' || ch === '\r') {
|
||||
// only write new lines, skip the whitespace
|
||||
accumulatedWS = 0;
|
||||
result += ch;
|
||||
} else {
|
||||
accumulatedWS++;
|
||||
}
|
||||
}
|
||||
result = append(result, ' ', accumulatedWS - after.length);
|
||||
result += after;
|
||||
return result;
|
||||
let accumulatedWS = 0;
|
||||
result += before;
|
||||
for (let i = start + before.length; i < end; i++) {
|
||||
let ch = oldContent[i];
|
||||
if (ch === '\n' || ch === '\r') {
|
||||
// only write new lines, skip the whitespace
|
||||
accumulatedWS = 0;
|
||||
result += ch;
|
||||
} else {
|
||||
accumulatedWS++;
|
||||
}
|
||||
}
|
||||
result = append(result, ' ', accumulatedWS - after.length);
|
||||
result += after;
|
||||
return result;
|
||||
}
|
||||
|
||||
function append(result: string, str: string, n: number): string {
|
||||
while (n > 0) {
|
||||
if (n & 1) {
|
||||
result += str;
|
||||
}
|
||||
n >>= 1;
|
||||
str += str;
|
||||
}
|
||||
return result;
|
||||
while (n > 0) {
|
||||
if (n & 1) {
|
||||
result += str;
|
||||
}
|
||||
n >>= 1;
|
||||
str += str;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -4,68 +4,61 @@ import { Node } from 'vscode-html-languageservice';
|
|||
|
||||
/** Normalizes a document URI */
|
||||
export function normalizeUri(uri: string): string {
|
||||
return URI.parse(uri).toString();
|
||||
return URI.parse(uri).toString();
|
||||
}
|
||||
|
||||
/** Turns a URL into a normalized FS Path */
|
||||
export function urlToPath(stringUrl: string): string | null {
|
||||
const url = URI.parse(stringUrl);
|
||||
if (url.scheme !== 'file') {
|
||||
return null;
|
||||
}
|
||||
return url.fsPath.replace(/\\/g, '/');
|
||||
const url = URI.parse(stringUrl);
|
||||
if (url.scheme !== 'file') {
|
||||
return null;
|
||||
}
|
||||
return url.fsPath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/** Converts a path to a URL */
|
||||
export function pathToUrl(path: string) {
|
||||
return URI.file(path).toString();
|
||||
return URI.file(path).toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* The language service is case insensitive, and would provide
|
||||
* hover info for Svelte components like `Option` which have
|
||||
* the same name like a html tag.
|
||||
*/
|
||||
*
|
||||
* The language service is case insensitive, and would provide
|
||||
* hover info for Svelte components like `Option` which have
|
||||
* the same name like a html tag.
|
||||
*/
|
||||
export function isPossibleComponent(node: Node): boolean {
|
||||
return !!node.tag?.[0].match(/[A-Z]/);
|
||||
return !!node.tag?.[0].match(/[A-Z]/);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* The language service is case insensitive, and would provide
|
||||
* hover info for Svelte components like `Option` which have
|
||||
* the same name like a html tag.
|
||||
*/
|
||||
*
|
||||
* The language service is case insensitive, and would provide
|
||||
* hover info for Svelte components like `Option` which have
|
||||
* the same name like a html tag.
|
||||
*/
|
||||
export function isPossibleClientComponent(node: Node): boolean {
|
||||
return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1;
|
||||
return isPossibleComponent(node) && (node.tag?.indexOf(':') ?? -1) > -1;
|
||||
}
|
||||
|
||||
/** Flattens an array */
|
||||
export function flatten<T>(arr: T[][]): T[] {
|
||||
return arr.reduce((all, item) => [...all, ...item], []);
|
||||
return arr.reduce((all, item) => [...all, ...item], []);
|
||||
}
|
||||
|
||||
/** Clamps a number between min and max */
|
||||
export function clamp(num: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, num));
|
||||
return Math.max(min, Math.min(max, num));
|
||||
}
|
||||
|
||||
/** Checks if a position is inside range */
|
||||
export function isInRange(positionToTest: Position, range: Range): boolean {
|
||||
return (
|
||||
isBeforeOrEqualToPosition(range.end, positionToTest) &&
|
||||
isBeforeOrEqualToPosition(positionToTest, range.start)
|
||||
);
|
||||
return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start);
|
||||
}
|
||||
|
||||
/** */
|
||||
export function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean {
|
||||
return (
|
||||
positionToTest.line < position.line ||
|
||||
(positionToTest.line === position.line && positionToTest.character <= position.character)
|
||||
);
|
||||
return positionToTest.line < position.line || (positionToTest.line === position.line && positionToTest.character <= position.character);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,23 +69,19 @@ export function isBeforeOrEqualToPosition(position: Position, positionToTest: Po
|
|||
* @param determineIfSame The function which determines if the previous invocation should be canceld or not
|
||||
* @param miliseconds Number of miliseconds to debounce
|
||||
*/
|
||||
export function debounceSameArg<T>(
|
||||
fn: (arg: T) => void,
|
||||
shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean,
|
||||
miliseconds: number
|
||||
): (arg: T) => void {
|
||||
let timeout: any;
|
||||
let prevArg: T | undefined;
|
||||
export function debounceSameArg<T>(fn: (arg: T) => void, shouldCancelPrevious: (newArg: T, prevArg?: T) => boolean, miliseconds: number): (arg: T) => void {
|
||||
let timeout: any;
|
||||
let prevArg: T | undefined;
|
||||
|
||||
return (arg: T) => {
|
||||
if (shouldCancelPrevious(arg, prevArg)) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
return (arg: T) => {
|
||||
if (shouldCancelPrevious(arg, prevArg)) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
prevArg = arg;
|
||||
timeout = setTimeout(() => {
|
||||
fn(arg);
|
||||
prevArg = undefined;
|
||||
}, miliseconds);
|
||||
};
|
||||
prevArg = arg;
|
||||
timeout = setTimeout(() => {
|
||||
fn(arg);
|
||||
prevArg = undefined;
|
||||
}, miliseconds);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,17 @@
|
|||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"lib": ["ESNext"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@astro-vscode/*": ["packages/*/src"]
|
||||
},
|
||||
},
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"lib": ["ESNext"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@astro-vscode/*": ["packages/*/src"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./packages/client"
|
||||
},
|
||||
{
|
||||
"path": "./packages/server"
|
||||
}
|
||||
]
|
||||
"extends": "./tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./packages/client"
|
||||
},
|
||||
{
|
||||
"path": "./packages/server"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
module.exports = {
|
||||
workspaceRoot: '../'
|
||||
};
|
||||
workspaceRoot: '../',
|
||||
};
|
||||
|
|
|
@ -1656,7 +1656,7 @@
|
|||
resolved "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz"
|
||||
integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.18.0":
|
||||
"@typescript-eslint/eslint-plugin@^4.22.0":
|
||||
version "4.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz#3d5f29bb59e61a9dba1513d491b059e536e16dbc"
|
||||
integrity sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==
|
||||
|
@ -4278,12 +4278,12 @@ escape-string-regexp@^2.0.0:
|
|||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz"
|
||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||
|
||||
eslint-config-prettier@^8.1.0:
|
||||
eslint-config-prettier@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
|
||||
integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==
|
||||
|
||||
eslint-plugin-prettier@^3.3.1:
|
||||
eslint-plugin-prettier@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7"
|
||||
integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==
|
||||
|
@ -4315,7 +4315,7 @@ eslint-visitor-keys@^2.0.0:
|
|||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz"
|
||||
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
|
||||
|
||||
eslint@^7.22.0:
|
||||
eslint@^7.25.0:
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67"
|
||||
integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==
|
||||
|
|
Loading…
Reference in a new issue