init
This commit is contained in:
parent
a86ba2ad41
commit
4ed5ec432a
39
.github/workflows/release.yml
vendored
Normal file
39
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||
publish: yarn release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Send a Slack notification if a publish happens
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
# You can do something when a publish happens.
|
||||
run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!"
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,4 +1,9 @@
|
||||
/node_modules
|
||||
/coverage
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules
|
||||
.turbo
|
||||
*.log
|
||||
.next
|
||||
*.local
|
||||
.env
|
||||
.cache
|
||||
storybook-static/
|
||||
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
auto-install-peers = true
|
||||
public-hoist-pattern[]=*storybook*
|
||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
||||
Copyright (c) <year> <owner> All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
193
README.md
193
README.md
@ -1,3 +1,192 @@
|
||||
# osr-package-template
|
||||
# Turborepo Design System Starter
|
||||
|
||||
Package basics
|
||||
This guide explains how to use a React design system starter powered by:
|
||||
|
||||
- 🏎 [Turborepo](https://turbo.build/repo) — High-performance build system for Monorepos
|
||||
- 🚀 [React](https://reactjs.org/) — JavaScript library for user interfaces
|
||||
- 🛠 [Tsup](https://github.com/egoist/tsup) — TypeScript bundler powered by esbuild
|
||||
- 📖 [Storybook](https://storybook.js.org/) — UI component environment powered by Vite
|
||||
|
||||
As well as a few others tools preconfigured:
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||
- [ESLint](https://eslint.org/) for code linting
|
||||
- [Prettier](https://prettier.io) for code formatting
|
||||
- [Changesets](https://github.com/changesets/changesets) for managing versioning and changelogs
|
||||
- [GitHub Actions](https://github.com/changesets/action) for fully automated package publishing
|
||||
|
||||
## Using this example
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
npx create-turbo@latest -e design-system
|
||||
```
|
||||
|
||||
### Useful Commands
|
||||
|
||||
- `pnpm build` - Build all packages, including the Storybook site
|
||||
- `pnpm dev` - Run all packages locally and preview with Storybook
|
||||
- `pnpm lint` - Lint all packages
|
||||
- `pnpm changeset` - Generate a changeset
|
||||
- `pnpm clean` - Clean up all `node_modules` and `dist` folders (runs each package's clean script)
|
||||
|
||||
## Turborepo
|
||||
|
||||
[Turborepo](https://turbo.build/repo) is a high-performance build system for JavaScript and TypeScript codebases. It was designed after the workflows used by massive software engineering organizations to ship code at scale. Turborepo abstracts the complex configuration needed for monorepos and provides fast, incremental builds with zero-configuration remote caching.
|
||||
|
||||
Using Turborepo simplifies managing your design system monorepo, as you can have a single lint, build, test, and release process for all packages. [Learn more](https://vercel.com/blog/monorepos-are-changing-how-teams-build-software) about how monorepos improve your development workflow.
|
||||
|
||||
## Apps & Packages
|
||||
|
||||
This Turborepo includes the following packages and applications:
|
||||
|
||||
- `apps/docs`: Component documentation site with Storybook
|
||||
- `packages/ui`: Core React components
|
||||
- `packages/utils`: Shared React utilities
|
||||
- `packages/typescript-config`: Shared `tsconfig.json`s used throughout the Turborepo
|
||||
- `packages/eslint-config`: ESLint preset
|
||||
|
||||
Each package and app is 100% [TypeScript](https://www.typescriptlang.org/). Workspaces enables us to "hoist" dependencies that are shared between packages to the root `package.json`. This means smaller `node_modules` folders and a better local dev experience. To install a dependency for the entire monorepo, use the `-w` workspaces flag with `pnpm add`.
|
||||
|
||||
This example sets up your `.gitignore` to exclude all generated files, other folders like `node_modules` used to store your dependencies.
|
||||
|
||||
### Compilation
|
||||
|
||||
To make the core library code work across all browsers, we need to compile the raw TypeScript and React code to plain JavaScript. We can accomplish this with `tsup`, which uses `esbuild` to greatly improve performance.
|
||||
|
||||
Running `pnpm build` from the root of the Turborepo will run the `build` command defined in each package's `package.json` file. Turborepo runs each `build` in parallel and caches & hashes the output to speed up future builds.
|
||||
|
||||
For `acme-core`, the `build` command is the following:
|
||||
|
||||
```bash
|
||||
tsup src/index.tsx --format esm,cjs --dts --external react
|
||||
```
|
||||
|
||||
`tsup` compiles `src/index.tsx`, which exports all of the components in the design system, into both ES Modules and CommonJS formats as well as their TypeScript types. The `package.json` for `acme-core` then instructs the consumer to select the correct format:
|
||||
|
||||
```json:acme-core/package.json
|
||||
{
|
||||
"name": "@acme/core",
|
||||
"version": "0.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
}
|
||||
```
|
||||
|
||||
Run `pnpm build` to confirm compilation is working correctly. You should see a folder `acme-core/dist` which contains the compiled output.
|
||||
|
||||
```bash
|
||||
acme-core
|
||||
└── dist
|
||||
├── index.d.ts <-- Types
|
||||
├── index.js <-- CommonJS version
|
||||
└── index.mjs <-- ES Modules version
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
Each file inside of `acme-core/src` is a component inside our design system. For example:
|
||||
|
||||
```tsx:acme-core/src/Button.tsx
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Button(props: ButtonProps) {
|
||||
return <button>{props.children}</button>;
|
||||
}
|
||||
|
||||
Button.displayName = 'Button';
|
||||
```
|
||||
|
||||
When adding a new file, ensure the component is also exported from the entry `index.tsx` file:
|
||||
|
||||
```tsx:acme-core/src/index.tsx
|
||||
import * as React from "react";
|
||||
export { Button, type ButtonProps } from "./Button";
|
||||
// Add new component exports here
|
||||
```
|
||||
|
||||
## Storybook
|
||||
|
||||
Storybook provides us with an interactive UI playground for our components. This allows us to preview our components in the browser and instantly see changes when developing locally. This example preconfigures Storybook to:
|
||||
|
||||
- Use Vite to bundle stories instantly (in milliseconds)
|
||||
- Automatically find any stories inside the `stories/` folder
|
||||
- Support using module path aliases like `@acme-core` for imports
|
||||
- Write MDX for component documentation pages
|
||||
|
||||
For example, here's the included Story for our `Button` component:
|
||||
|
||||
```js:apps/docs/stories/button.stories.mdx
|
||||
import { Button } from '@acme-core/src';
|
||||
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||
|
||||
<Meta title="Components/Button" component={Button} />
|
||||
|
||||
# Button
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget consectetur tempor, nisl nunc egestas nisi, euismod aliquam nisl nunc euismod.
|
||||
|
||||
## Props
|
||||
|
||||
<Props of={Box} />
|
||||
|
||||
## Examples
|
||||
|
||||
<Preview>
|
||||
<Story name="Default">
|
||||
<Button>Hello</Button>
|
||||
</Story>
|
||||
</Preview>
|
||||
```
|
||||
|
||||
This example includes a few helpful Storybook scripts:
|
||||
|
||||
- `pnpm dev`: Starts Storybook in dev mode with hot reloading at `localhost:6006`
|
||||
- `pnpm build`: Builds the Storybook UI and generates the static HTML files
|
||||
- `pnpm preview-storybook`: Starts a local server to view the generated Storybook UI
|
||||
|
||||
## Versioning & Publishing Packages
|
||||
|
||||
This example uses [Changesets](https://github.com/changesets/changesets) to manage versions, create changelogs, and publish to npm. It's preconfigured so you can start publishing packages immediately.
|
||||
|
||||
You'll need to create an `NPM_TOKEN` and `GITHUB_TOKEN` and add it to your GitHub repository settings to enable access to npm. It's also worth installing the [Changesets bot](https://github.com/apps/changeset-bot) on your repository.
|
||||
|
||||
### Generating the Changelog
|
||||
|
||||
To generate your changelog, run `pnpm changeset` locally:
|
||||
|
||||
1. **Which packages would you like to include?** – This shows which packages and changed and which have remained the same. By default, no packages are included. Press `space` to select the packages you want to include in the `changeset`.
|
||||
1. **Which packages should have a major bump?** – Press `space` to select the packages you want to bump versions for.
|
||||
1. If doing the first major version, confirm you want to release.
|
||||
1. Write a summary for the changes.
|
||||
1. Confirm the changeset looks as expected.
|
||||
1. A new Markdown file will be created in the `changeset` folder with the summary and a list of the packages included.
|
||||
|
||||
### Releasing
|
||||
|
||||
When you push your code to GitHub, the [GitHub Action](https://github.com/changesets/action) will run the `release` script defined in the root `package.json`:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=docs^... && changeset publish
|
||||
```
|
||||
|
||||
Turborepo runs the `build` script for all publishable packages (excluding docs) and publishes the packages to npm. By default, this example includes `acme` as the npm organization. To change this, do the following:
|
||||
|
||||
- Rename folders in `packages/*` to replace `acme` with your desired scope
|
||||
- Search and replace `acme` with your desired scope
|
||||
- Re-run `pnpm install`
|
||||
|
||||
To publish packages to a private npm organization scope, **remove** the following from each of the `package.json`'s
|
||||
|
||||
```diff
|
||||
- "publishConfig": {
|
||||
- "access": "public"
|
||||
- },
|
||||
```
|
||||
|
||||
93
eslint.config.js
Normal file
93
eslint.config.js
Normal file
@ -0,0 +1,93 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// plugins
|
||||
import regexpEslint from 'eslint-plugin-regexp';
|
||||
const typescriptEslint = tseslint.plugin;
|
||||
|
||||
// parsers
|
||||
const typescriptParser = tseslint.parser;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{ files: ["src/*.{js,mjs,cjs,ts}"] },
|
||||
// { languageOptions: { globals: globals.browser } },
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
regexpEslint.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: ['./packages/*/tsconfig.json', './tsconfig.eslint.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
regexp: regexpEslint,
|
||||
},
|
||||
rules: {
|
||||
// These off/configured-differently-by-default rules fit well for us
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison' : 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
// Todo: do we want these?
|
||||
'no-var': 'off',
|
||||
|
||||
'regexp/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/class-literal-property-style': 'off',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/only-throw-error': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/sort-type-constituents': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
// Used by Biome
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
// These rules enabled by the preset configs don't work well for us
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'prefer-const': 'off',
|
||||
|
||||
// In some cases, using explicit letter-casing is more performant than the `i` flag
|
||||
'regexp/use-ignore-case': 'off',
|
||||
'regexp/prefer-regexp-exec': 'warn',
|
||||
'regexp/prefer-regexp-test': 'warn',
|
||||
'no-control-regex': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
55
package.json
55
package.json
@ -1,45 +1,20 @@
|
||||
{
|
||||
"name": "@plastichub/template",
|
||||
"description": "",
|
||||
"version": "0.3.1",
|
||||
"main": "main.js",
|
||||
"typings": "index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bin": {
|
||||
"osr-bin": "main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^14.17.5",
|
||||
"@types/yargs": "^17.0.2",
|
||||
"chalk": "^2.4.1",
|
||||
"convert-units": "^2.3.4",
|
||||
"env-var": "^7.0.1",
|
||||
"typescript": "^4.3.5",
|
||||
"yargs": "^14.2.3",
|
||||
"yargs-parser": "^15.0.3"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "tsc; mocha --full-trace mocha \"spec/**/*.spec.js\"",
|
||||
"test-with-coverage": "istanbul cover node_modules/.bin/_mocha -- 'spec/**/*.spec.js'",
|
||||
"lint": "tslint --project=./tsconfig.json",
|
||||
"build": "tsc -p .",
|
||||
"dev": "tsc -p . --declaration -w",
|
||||
"typings": "tsc --declaration",
|
||||
"docs": "npx typedoc src/index.ts",
|
||||
"dev-test-watch": "mocha-typescript-watch"
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"clean": "turbo run clean && rm -rf node_modules",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"release": "turbo run build --filter=docs^... && changeset publish"
|
||||
},
|
||||
"homepage": "https://git.osr-plastic.org/plastichub/lib-content",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.osr-plastic.org/plastichub/lib-content.git"
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"prettier": "^3.2.5",
|
||||
"turbo": "^2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
]
|
||||
"packageManager": "pnpm@8.15.6",
|
||||
"name": "design-system"
|
||||
}
|
||||
|
||||
39
packages/commons/.gitignore
vendored
Normal file
39
packages/commons/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
node_modules
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
5
packages/commons/.npmignore
Normal file
5
packages/commons/.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
src
|
||||
package-lock.json
|
||||
docs
|
||||
scripts
|
||||
29
packages/commons/LICENSE
Normal file
29
packages/commons/LICENSE
Normal file
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017,
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
2
packages/commons/README.md
Normal file
2
packages/commons/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# core
|
||||
core stuff
|
||||
95
packages/commons/eslint.config.js
Normal file
95
packages/commons/eslint.config.js
Normal file
@ -0,0 +1,95 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// plugins
|
||||
import regexpEslint from 'eslint-plugin-regexp';
|
||||
const typescriptEslint = tseslint.plugin;
|
||||
|
||||
// parsers
|
||||
const typescriptParser = tseslint.parser;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
files: ["src/*.{ts}"]
|
||||
},
|
||||
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
regexpEslint.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: ['./packages/*/tsconfig.json', './tsconfig.eslint.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
regexp: regexpEslint,
|
||||
},
|
||||
rules: {
|
||||
// These off/configured-differently-by-default rules fit well for us
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison' : 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
// Todo: do we want these?
|
||||
'no-var': 'off',
|
||||
|
||||
'regexp/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/class-literal-property-style': 'off',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/only-throw-error': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/sort-type-constituents': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
// Used by Biome
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
// These rules enabled by the preset configs don't work well for us
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'prefer-const': 'off',
|
||||
|
||||
// In some cases, using explicit letter-casing is more performant than the `i` flag
|
||||
'regexp/use-ignore-case': 'off',
|
||||
'regexp/prefer-regexp-exec': 'warn',
|
||||
'regexp/prefer-regexp-test': 'warn',
|
||||
'no-control-regex': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
57
packages/commons/package.json
Normal file
57
packages/commons/package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@polymech/commons",
|
||||
"version": "0.2.6",
|
||||
"license": "BSD",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "BSD",
|
||||
"url": "https://git.osr-plastic.org/osr-plastic/osr-core/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.osr-plastic.org/osr-plastic/osr-core.git"
|
||||
},
|
||||
"types": "index.d.ts",
|
||||
"dependencies": {
|
||||
"tslog": "^3.3.3",
|
||||
"tsup": "^8.3.5",
|
||||
"zod": "^3.24.1",
|
||||
"@polymech/fs": "workspace:*",
|
||||
"@polymech/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^8.10.66",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-regexp": "^2.7.0",
|
||||
"globals": "^15.14.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tsc && mocha build/test",
|
||||
"buildtsc": "tsc -p . --declaration",
|
||||
"build": "tsup",
|
||||
"start": "node build/index.js",
|
||||
"typings": "tsc -p . --declaration",
|
||||
"dev": "tsc -p . --declaration -w"
|
||||
},
|
||||
"modules": [],
|
||||
"readmeFilename": "Readme.md"
|
||||
}
|
||||
218
packages/commons/src/shemas/index.ts
Normal file
218
packages/commons/src/shemas/index.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import * as path from 'node:path'
|
||||
import * as CLI from 'yargs'
|
||||
import { z, ZodTypeAny, ZodObject, ZodEffects, ZodOptional, ZodDefault } from 'zod'
|
||||
import { sync as writeFS } from '@polymech/fs/write'
|
||||
import { zodToTs, printNode } from 'zod-to-ts'
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { logger } from '../logger'
|
||||
|
||||
type InnerType<T> = T extends ZodEffects<infer U> ? InnerType<U> : T
|
||||
|
||||
type GetInnerType<T extends ZodTypeAny> = T extends ZodObject<any>
|
||||
? T
|
||||
: T extends ZodEffects<ZodObject<any>>
|
||||
? InnerType<T>
|
||||
: never;
|
||||
|
||||
export * from './path'
|
||||
|
||||
export const generate_interfaces = (schemas: ZodObject<any>[], dst: string) => {
|
||||
const types = schemas.map(schema => `export interface ${schema.description || 'IOptions'} ${printNode(zodToTs(schema).node)}`)
|
||||
writeFS(dst, types.join('\n'))
|
||||
}
|
||||
export const enumerateHelpStrings = (schema: ZodTypeAny, path: string[] = [], logger: any): void => {
|
||||
if (schema instanceof ZodObject) {
|
||||
for (const key in schema.shape) {
|
||||
const nestedSchema = schema.shape[key];
|
||||
enumerateHelpStrings(nestedSchema, [...path, key], logger)
|
||||
}
|
||||
} else {
|
||||
const description = schema._def.description;
|
||||
if (description) {
|
||||
logger.debug(`\t ${path.join('.')}: ${description}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
export const yargsDefaults = (yargs: CLI.Argv) => yargs.parserConfiguration({ "camel-case-expansion": false })
|
||||
|
||||
export const getInnerSchema = (schema: ZodTypeAny): ZodTypeAny => {
|
||||
while (schema instanceof ZodEffects) {
|
||||
schema = schema._def.schema
|
||||
}
|
||||
return schema
|
||||
}
|
||||
export const getInnerType = (type: ZodTypeAny) => {
|
||||
while (type instanceof ZodOptional) {
|
||||
type = type._def.innerType
|
||||
}
|
||||
while (type._def.typeName === 'ZodDefault' || type._def.typeName === 'ZodOptional') {
|
||||
type = type._def.innerType;
|
||||
}
|
||||
return type._def.typeName
|
||||
}
|
||||
export const getDefaultValue = (schema: ZodTypeAny) => {
|
||||
if (schema instanceof ZodDefault) {
|
||||
return schema._def.defaultValue();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const getFieldDefaultValue = (schema: ZodTypeAny): any | undefined => {
|
||||
if(!schema){
|
||||
return undefined
|
||||
}
|
||||
if (schema._def.typeName === 'ZodDefault') {
|
||||
return schema._def.defaultValue();
|
||||
}
|
||||
if (schema instanceof ZodOptional) {
|
||||
return getFieldDefaultValue(schema.unwrap());
|
||||
}
|
||||
if (schema instanceof ZodEffects) {
|
||||
return getFieldDefaultValue(schema._def.schema);
|
||||
}
|
||||
if(typeof schema._def){
|
||||
return getFieldDefaultValue(schema._def.schema)
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const getDescription = (schema: ZodTypeAny): string | undefined =>{
|
||||
if(!schema){
|
||||
return undefined
|
||||
}
|
||||
if (schema._def.description) {
|
||||
return schema._def.description;
|
||||
}
|
||||
if (schema instanceof ZodOptional) {
|
||||
return getDescription(schema.unwrap());
|
||||
}
|
||||
|
||||
if (schema instanceof ZodEffects) {
|
||||
return getDescription(schema._def.schema);
|
||||
}
|
||||
|
||||
if(typeof schema._def){
|
||||
return getDescription(schema._def.schema)
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const toYargs = (yargs: CLI.Argv, zodSchema: ZodObject<any>, options?: {
|
||||
onKey?: (yargs: CLI.Argv, key: string, options:any) => any
|
||||
}) => {
|
||||
yargsDefaults(yargs)
|
||||
try {
|
||||
const shape = zodSchema.shape
|
||||
for (const key in shape) {
|
||||
const zodField = shape[key] as ZodTypeAny
|
||||
const innerDef = getInnerSchema(zodField)
|
||||
if (!innerDef) {
|
||||
continue
|
||||
}
|
||||
let type: 'string' | 'boolean' | 'number' | undefined;
|
||||
const inner_type = getInnerType(innerDef)
|
||||
let descriptionExtra = ''
|
||||
switch (inner_type) {
|
||||
case 'ZodString':
|
||||
type = 'string'
|
||||
break
|
||||
case 'ZodBoolean':
|
||||
type = 'boolean'
|
||||
break
|
||||
case 'ZodNumber':
|
||||
type = 'number'
|
||||
break
|
||||
case 'ZodOptional':
|
||||
case 'ZodEnum':
|
||||
type = getInnerType(innerDef)
|
||||
if (innerDef._def.typeName === 'ZodEnum') {
|
||||
descriptionExtra = `\n\t ${innerDef._def.values.join(' \n\t ')}`
|
||||
}
|
||||
break
|
||||
}
|
||||
const defaultValue = getFieldDefaultValue(zodField)
|
||||
let handled = false
|
||||
const args = {
|
||||
type,
|
||||
default: defaultValue,
|
||||
describe: `${zodField._def.description || ''} ${descriptionExtra}`.trim()
|
||||
}
|
||||
if(options?.onKey){
|
||||
handled = options.onKey(yargs, key, args)
|
||||
}
|
||||
if(!handled){
|
||||
yargs.option(key,args)
|
||||
}
|
||||
}
|
||||
return yargs
|
||||
} catch (error) {
|
||||
logger.error('Error processing schema:', error)
|
||||
return yargs
|
||||
}
|
||||
}
|
||||
/////////////////////////////////////////////////////////
|
||||
//
|
||||
// Schema Writers
|
||||
//
|
||||
const extension = (file: string) => path.parse(file).ext
|
||||
const json = (data: any, file: string, name: string, options: {}) => writeFS(file, data.map((s) => zodToJsonSchema(s, name)))
|
||||
|
||||
export const WRITERS =
|
||||
{
|
||||
'.json': json
|
||||
}
|
||||
|
||||
export const writer = (file: string) => WRITERS[extension(file)]
|
||||
|
||||
export const write = (schemas: ZodObject<any>[], file: string, name: string, options: {}) => {
|
||||
if (!WRITERS[extension(file)]) {
|
||||
logger.error(`No writer found for file extension: ${extension(file)} : file: ${file}`)
|
||||
return
|
||||
}
|
||||
logger.debug(`Writing schema to ${file} : ${name}`)
|
||||
try {
|
||||
writer(file)(schemas, file, name, options)
|
||||
} catch (e) {
|
||||
logger.trace(`Error writing schema to ${file} : ${name}`, e, e.stack, e.message)
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Schema Combinators
|
||||
export const combineValidatorsOr = (validators: z.ZodTypeAny[]) => {
|
||||
return z.string().refine((value) => {
|
||||
const errors = [];
|
||||
const isValid = validators.some((validator) => {
|
||||
try {
|
||||
validator.parse(value)
|
||||
return true;
|
||||
} catch (err) {
|
||||
errors.push(err.errors)
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!isValid) {
|
||||
throw new z.ZodError(errors.flat())
|
||||
}
|
||||
return true;
|
||||
}, 'Invalid value for all provided validators')
|
||||
}
|
||||
|
||||
export const combineValidatorsOrUsingZod = (validators: z.ZodTypeAny[]) => {
|
||||
return validators.reduce((acc, validator) => acc.or(validator));
|
||||
};
|
||||
export const combineValidatorsOrUsingZod2 = (validators: z.ZodTypeAny[]) => {
|
||||
return validators.reduce((acc, validator) => {
|
||||
return acc.or(validator).refine((value) => {
|
||||
try {
|
||||
acc.parse(value);
|
||||
return true;
|
||||
} catch (errAcc) {
|
||||
try {
|
||||
validator.parse(value);
|
||||
return true;
|
||||
} catch (errValidator) {
|
||||
throw new z.ZodError([...errAcc.errors, ...errValidator.errors]);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
export * from './zod_map'
|
||||
20
packages/commons/src/shemas/openapi.ts
Normal file
20
packages/commons/src/shemas/openapi.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
export const openapi = (data: ZodObject<any>[], file: string, name: string, options: {}) => {
|
||||
const registry = new OpenAPIRegistry()
|
||||
data.forEach((s) => registry.register(s.description, s))
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions)
|
||||
const component = generator.generateComponents()
|
||||
// const content = stringifyYAML(component)
|
||||
return component
|
||||
}
|
||||
*/
|
||||
/*
|
||||
const yaml = (data: ZodObject<any>[], file: string, name: string, options: {}) => {
|
||||
const registry = new OpenAPIRegistry()
|
||||
data.forEach((s) => registry.register(s.description, s))
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions)
|
||||
const component = generator.generateComponents()
|
||||
logger.debug(`Writing schema to ${file} : ${name}`,component)
|
||||
writeFS(file,stringifyYAML(component))
|
||||
}
|
||||
*/
|
||||
256
packages/commons/src/shemas/path.ts
Normal file
256
packages/commons/src/shemas/path.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import { z, ZodTypeAny } from 'zod'
|
||||
import * as path from 'path'
|
||||
import { accessSync, constants, lstatSync, existsSync } from 'fs'
|
||||
|
||||
import { isString } from '@polymech/'
|
||||
|
||||
import { logger } from '../logger'
|
||||
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
|
||||
import { DEFAULT_VARS, resolve, resolveVariables } from '../variables'
|
||||
|
||||
import { getDescription } from '../'
|
||||
import { isFile } from '../lib/fs'
|
||||
|
||||
|
||||
type TResult = { resolved: string, source: string, value: unknown }
|
||||
type TRefine = (src: string, ctx: any, variables: Record<string, string>) => string | z.ZodNever
|
||||
type TTransform = (src: string, variables?: Record<string, string>) => string | TResult
|
||||
type TExtend = { refine: Array<TRefine>, transform: Array<TTransform> }
|
||||
|
||||
const DefaultPathSchemaBase = z.string().describe('Path to a file or directory')
|
||||
|
||||
const PathErrorMessages = {
|
||||
INVALID_INPUT: 'INVALID_INPUT: ${inputPath}',
|
||||
PATH_DOES_NOT_EXIST: 'Path does not exist ${inputPath} = ${resolvedPath}',
|
||||
DIRECTORY_NOT_WRITABLE: 'Directory is not writable ${inputPath} = ${resolvedPath}',
|
||||
NOT_A_DIRECTORY: 'Path is not a directory or does not exist ${inputPath} = ${resolvedPath}',
|
||||
NOT_A_JSON_FILE: 'File is not a JSON file or does not exist ${inputPath} = ${resolvedPath}',
|
||||
PATH_NOT_ABSOLUTE: 'Path is not absolute ${inputPath} = ${resolvedPath}',
|
||||
PATH_NOT_RELATIVE: 'Path is not relative ${inputPath} = ${resolvedPath}',
|
||||
} as const
|
||||
|
||||
export enum E_PATH {
|
||||
ENSURE_PATH_EXISTS = 1,
|
||||
INVALID_INPUT,
|
||||
ENSURE_DIRECTORY_WRITABLE,
|
||||
ENSURE_FILE_IS_JSON,
|
||||
ENSURE_PATH_IS_ABSOLUTE,
|
||||
ENSURE_PATH_IS_RELATIVE,
|
||||
GET_PATH_INFO
|
||||
}
|
||||
export const Transformers = {
|
||||
resolve: (val: string, variables: Record<string, string> = {}) => {
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
resolved: path.resolve(resolve(val, false, variables)),
|
||||
source: val
|
||||
}
|
||||
},
|
||||
json: (val: string | { resolved: string, source: string }, variables: Record<string, string> = {}) => {
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
const resolved = path.resolve(resolve(isString(val) ? val : val.source, false, variables))
|
||||
return {
|
||||
resolved,
|
||||
source: val,
|
||||
value: read(resolved, 'json')
|
||||
}
|
||||
},
|
||||
string: (val: string | { resolved: string, source: string }, variables: Record<string, string> = {}) => {
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
let src = isString(val) ? val : val.source
|
||||
src = resolve(src, false, variables)
|
||||
const resolved = path.resolve(src)
|
||||
if (!exists(resolved) || !isFile(resolved)) {
|
||||
return {
|
||||
resolved,
|
||||
source: val,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
else {
|
||||
let value = null
|
||||
try {
|
||||
value = read(resolved, 'string')
|
||||
} catch (e) {
|
||||
logger.error('Failed to read file', { resolved, source: val, error: e.message })
|
||||
}
|
||||
return {
|
||||
resolved,
|
||||
source: val,
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TransformersDescription = [
|
||||
{
|
||||
description: 'RESOLVE_PATH',
|
||||
fn: Transformers.resolve
|
||||
},
|
||||
{
|
||||
description: 'READ_JSON',
|
||||
fn: Transformers.json
|
||||
},
|
||||
{
|
||||
description: 'READ_STRING',
|
||||
fn: Transformers.string
|
||||
}
|
||||
]
|
||||
const extendType = (type: ZodTypeAny, extend: TExtend, variables: Record<string, string> = {}) => {
|
||||
if (Array.isArray(extend.refine)) {
|
||||
for (const refine of extend.refine) {
|
||||
type = type.refine(refine as any)
|
||||
}
|
||||
} else {
|
||||
type = type.refine(extend.refine)
|
||||
}
|
||||
if (Array.isArray(extend.transform)) {
|
||||
for (const transform of extend.transform) {
|
||||
type = type.transform((val) => transform(val, variables))
|
||||
}
|
||||
} else {
|
||||
type = type.transform(extend.transform)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
const extendTypeDescription = (type: ZodTypeAny, extension: TExtend, variables: Record<string, string> = {}) => {
|
||||
const description = getDescription(type) || ''
|
||||
let transformerDescriptions = 'Transformers:\n'
|
||||
if (Array.isArray(extension.transform)) {
|
||||
for (const transform of extension.transform) {
|
||||
transformerDescriptions += transformerDescription(transform) + '\n'
|
||||
}
|
||||
} else {
|
||||
transformerDescriptions += transformerDescription(extension.transform) + '\n'
|
||||
}
|
||||
type = type.describe(description + '\n' + transformerDescriptions)
|
||||
return type
|
||||
}
|
||||
|
||||
const transformerDescription = (fn: TTransform) => {
|
||||
const description = TransformersDescription.find((t) => t.fn === fn)
|
||||
return description ? description.description : 'Unknown'
|
||||
}
|
||||
|
||||
export const extendSchema = (baseSchema: z.ZodObject<any>, extend: Record<string, any>) => {
|
||||
const baseShape = baseSchema.shape
|
||||
const extendedShape: Record<string, ZodTypeAny> = { ...baseShape }
|
||||
for (const [key, refines] of Object.entries(extend)) {
|
||||
if (!baseShape[key])
|
||||
continue
|
||||
|
||||
let fieldSchema = baseShape[key]
|
||||
if (Array.isArray(refines.refine)) {
|
||||
for (const refine of refines.refine) {
|
||||
fieldSchema = fieldSchema.superRefine(refine)
|
||||
}
|
||||
} else {
|
||||
fieldSchema = fieldSchema.superRefine(refines)
|
||||
}
|
||||
if (Array.isArray(refines.transform)) {
|
||||
for (const transform of refines.transform) {
|
||||
fieldSchema = fieldSchema.transform((val) => transform(val))
|
||||
}
|
||||
} else {
|
||||
fieldSchema = fieldSchema.transform(refines.transform)
|
||||
}
|
||||
extendedShape[key] = fieldSchema
|
||||
|
||||
}
|
||||
return z.object(extendedShape)
|
||||
}
|
||||
|
||||
export const ENSURE_DIRECTORY_WRITABLE = (inputPath: string, ctx: any, variables: Record<string, string>) => {
|
||||
const resolvedPath = path.resolve(resolve(inputPath, false, variables))
|
||||
const parts = path.parse(resolvedPath)
|
||||
if (resolvedPath && existsSync(parts.dir) && lstatSync(parts.dir).isDirectory()) {
|
||||
try {
|
||||
accessSync(resolvedPath, constants.W_OK)
|
||||
return resolvedPath
|
||||
} catch (e) {
|
||||
ctx.addIssue({
|
||||
code: E_PATH.ENSURE_DIRECTORY_WRITABLE,
|
||||
message: resolveVariables(PathErrorMessages.DIRECTORY_NOT_WRITABLE, false, { inputPath, resolvedPath })
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
} else {
|
||||
ctx.addIssue({
|
||||
code: E_PATH.ENSURE_DIRECTORY_WRITABLE,
|
||||
message: resolveVariables(PathErrorMessages.NOT_A_DIRECTORY, false, { inputPath, resolvedPath })
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
|
||||
}
|
||||
export const IS_VALID_STRING = (inputPath: string) => {
|
||||
return isString(inputPath)
|
||||
}
|
||||
export const ENSURE_PATH_EXISTS = (inputPath: string, ctx: any, variables: Record<string, string>) => {
|
||||
if (!inputPath || !ctx) {
|
||||
return z.NEVER
|
||||
}
|
||||
if (!isString(inputPath)) {
|
||||
ctx.addIssue({
|
||||
code: E_PATH.INVALID_INPUT,
|
||||
message: resolveVariables(PathErrorMessages.INVALID_INPUT, false, {})
|
||||
})
|
||||
return z.NEVER
|
||||
}
|
||||
const resolvedPath = path.resolve(resolve(inputPath, false, variables))
|
||||
if (!exists(resolvedPath)) {
|
||||
ctx.addIssue({
|
||||
code: E_PATH.ENSURE_PATH_EXISTS,
|
||||
message: resolveVariables(PathErrorMessages.PATH_DOES_NOT_EXIST, false, { inputPath, resolvedPath })
|
||||
})
|
||||
|
||||
return z.NEVER
|
||||
}
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
export const test = () => {
|
||||
const BaseCompilerOptions = () => z.object({
|
||||
root: DefaultPathSchemaBase.default(`${process.cwd()}`)
|
||||
})
|
||||
const ret = extendSchema(BaseCompilerOptions(), {
|
||||
root: {
|
||||
refine: [
|
||||
(val, ctx) => ENSURE_DIRECTORY_WRITABLE(val, ctx, DEFAULT_VARS({ exampleVar: 'exampleValue' })),
|
||||
(val, ctx) => ENSURE_PATH_EXISTS(val, ctx, DEFAULT_VARS({ exampleVar: 'exampleValue' }))
|
||||
],
|
||||
transform: [
|
||||
(val) => path.resolve(resolve(val, false, DEFAULT_VARS({ exampleVar: 'exampleValue' })))
|
||||
]
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
export const Templates =
|
||||
{
|
||||
json: {
|
||||
refine: [IS_VALID_STRING, ENSURE_PATH_EXISTS],
|
||||
transform: [Transformers.resolve, Transformers.json]
|
||||
},
|
||||
string: {
|
||||
refine: [ENSURE_PATH_EXISTS],
|
||||
transform: [Transformers.resolve, Transformers.string]
|
||||
}
|
||||
}
|
||||
|
||||
export const extend = (baseSchema: ZodTypeAny, template: any, variables: Record<string, string> = {}) => {
|
||||
const type = extendType(baseSchema, template, variables)
|
||||
return extendTypeDescription(type, template, variables)
|
||||
}
|
||||
187
packages/commons/src/shemas/types.ts
Normal file
187
packages/commons/src/shemas/types.ts
Normal file
@ -0,0 +1,187 @@
|
||||
export enum FLAG {
|
||||
/**
|
||||
* Instruct for no additional extra processing
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
NONE = 0x00000000,
|
||||
/**
|
||||
* Will instruct the pre/post processor to base-64 decode or encode
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
BASE_64 = 0x00000001,
|
||||
/**
|
||||
* Post/Pre process the value with a user function
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
USE_FUNCTION = 0x00000002,
|
||||
/**
|
||||
* Replace variables with local scope's variables during the post/pre process
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
REPLACE_VARIABLES = 0x00000004,
|
||||
/**
|
||||
* Replace variables with local scope's variables during the post/pre process but evaluate the whole string
|
||||
* as Javascript
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
REPLACE_VARIABLES_EVALUATED = 0x00000008,
|
||||
/**
|
||||
* Will instruct the pre/post processor to escpape evaluated or replaced variables or expressions
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
ESCAPE = 0x00000010,
|
||||
/**
|
||||
* Will instruct the pre/post processor to replace block calls with oridinary vanilla script
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
REPLACE_BLOCK_CALLS = 0x00000020,
|
||||
/**
|
||||
* Will instruct the pre/post processor to remove variable delimitters/placeholders from the final string
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
REMOVE_DELIMTTERS = 0x00000040,
|
||||
/**
|
||||
* Will instruct the pre/post processor to remove "[" ,"]" , "(" , ")" , "{", "}" , "*" , "+" , "."
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
ESCAPE_SPECIAL_CHARS = 0x00000080,
|
||||
/**
|
||||
* Will instruct the pre/post processor to use regular expressions over string substitution
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
USE_REGEX = 0x00000100,
|
||||
/**
|
||||
* Will instruct the pre/post processor to use Filtrex (custom bison parser, needs xexpression) over string substitution
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
USE_FILTREX = 0x00000200,
|
||||
/**
|
||||
* Cascade entry. There are cases where #USE_FUNCTION is not enough or we'd like to avoid further type checking.
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
CASCADE = 0x00000400,
|
||||
/**
|
||||
* Cascade entry. There are cases where #USE_FUNCTION is not enough or we'd like to avoid further type checking.
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
EXPRESSION = 0x00000800,
|
||||
/**
|
||||
* Dont parse anything
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
DONT_PARSE = 0x000001000,
|
||||
/**
|
||||
* Convert to hex
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
TO_HEX = 0x000002000,
|
||||
/**
|
||||
* Convert to hex
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
REPLACE_HEX = 0x000004000,
|
||||
/**
|
||||
* Wait for finish
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
WAIT = 0x000008000,
|
||||
/**
|
||||
* Wait for finish
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
DONT_ESCAPE = 0x000010000,
|
||||
/**
|
||||
* Flag to mark the maximum core bit mask, after here its user land
|
||||
* @constant
|
||||
* @type int
|
||||
*/
|
||||
END = 0x000020000
|
||||
}
|
||||
|
||||
export enum EType
|
||||
{
|
||||
Number = 'Number',
|
||||
String = 'String',
|
||||
Boolean = 'Boolean',
|
||||
Date = 'Date',
|
||||
TimeStamp = 'TimeStamp',
|
||||
Duration = 'Duration',
|
||||
Url = 'Url',
|
||||
UrlScheme = 'Url-Scheme',
|
||||
Asset = 'Asset',
|
||||
Symbol = 'Symbol',
|
||||
Value = 'Value',
|
||||
Values = 'Values',
|
||||
Attribute = 'Attribute',
|
||||
Parameter = 'Parameter',
|
||||
Operation = 'Operation',
|
||||
ParameterOperation = 'ParameterOperation',
|
||||
Template = 'Template',
|
||||
Arguments = 'Arguments'
|
||||
}
|
||||
export type TVector2D = [number, number];
|
||||
export type TVector3D = [number, number, number];
|
||||
export type TBBox = [TVector3D, TVector3D];
|
||||
export type TQuaternion = [number, number, number, number];
|
||||
export type TFlags = Record<string, bigint>;
|
||||
export type TExpression = string | [string | RegExp, { [key: string]: any }];
|
||||
export type TOptions = { flags?: TFlags | { [key: string]: any } };
|
||||
|
||||
export interface IUrlScheme {
|
||||
url: string;
|
||||
options?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface IAsset {
|
||||
urlScheme: IUrlScheme;
|
||||
options?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export type TSelector = TExpression | [TExpression, { [key: string]: any }];
|
||||
|
||||
export interface ITypeInfo {
|
||||
type: string;
|
||||
symbol: bigint;
|
||||
}
|
||||
|
||||
export interface IRef {
|
||||
key: string | string;
|
||||
struct: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface IAttribute {
|
||||
type: ITypeInfo;
|
||||
value: bigint;
|
||||
}
|
||||
|
||||
export interface IParameter {
|
||||
type: ITypeInfo;
|
||||
value: bigint;
|
||||
}
|
||||
|
||||
export interface IParameterOperation {
|
||||
param1: bigint;
|
||||
param2: bigint;
|
||||
operation: bigint;
|
||||
}
|
||||
|
||||
export type TTemplate = string | [ITypeInfo | TSelector, { [key: string]: any }];
|
||||
export type TArguments = { [key: string]: any } | any[];
|
||||
1
packages/commons/src/shemas/vfs.ts
Normal file
1
packages/commons/src/shemas/vfs.ts
Normal file
@ -0,0 +1 @@
|
||||
//import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
112
packages/commons/src/shemas/zod_map.ts
Normal file
112
packages/commons/src/shemas/zod_map.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { z, ZodObject, ZodTypeAny } from 'zod';
|
||||
|
||||
/**
|
||||
* Manages a collection of Zod schema properties
|
||||
* and combines them into a single Zod object schema.
|
||||
*
|
||||
* @template MetaType The type of metadata you want to store for each field.
|
||||
* Defaults to Record<string, unknown> if not provided.
|
||||
*/
|
||||
export class ZodMetaMap<MetaType = Record<string, unknown>> {
|
||||
private fieldMap = new Map<
|
||||
string,
|
||||
{ schema: ZodTypeAny; metadata?: MetaType }
|
||||
>();
|
||||
|
||||
/**
|
||||
* Adds a Zod schema under a specific key (property name),
|
||||
* optionally attaching typed metadata.
|
||||
*
|
||||
* @param key - The name of the property in the root object.
|
||||
* @param schema - The Zod schema for that property.
|
||||
* @param metadata - Optional metadata object (type MetaType).
|
||||
*/
|
||||
add<T extends ZodTypeAny>(key: string, schema: T, metadata?: MetaType): this {
|
||||
this.fieldMap.set(key, { schema, metadata });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a root Zod object
|
||||
* that combines all properties which were added.
|
||||
*/
|
||||
root(): ZodObject<Record<string, ZodTypeAny>> {
|
||||
const shape: Record<string, ZodTypeAny> = {};
|
||||
for (const [key, { schema }] of this.fieldMap.entries()) {
|
||||
shape[key] = schema;
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the metadata for a specific key, if any.
|
||||
*/
|
||||
getMetadata(key: string): MetaType | undefined {
|
||||
return this.fieldMap.get(key)?.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method: creates a SchemaMetaManager
|
||||
* while letting you optionally specify the MetaType.
|
||||
*
|
||||
* Usage:
|
||||
* const manager = SchemaMetaManager.create<MyFieldMeta>();
|
||||
*/
|
||||
static create<MT = Record<string, unknown>>(): ZodMetaMap<MT> {
|
||||
return new ZodMetaMap<MT>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a basic UiSchema object that RJSF can use to render form controls.
|
||||
*
|
||||
* - Adds a top-level "ui:submitButtonOptions" (example).
|
||||
* - For each field, we set `ui:title` (uppercase key),
|
||||
* `ui:description` (from Zod's .describe() if available),
|
||||
* and a naive placeholder from the default value (if parse(undefined) succeeds).
|
||||
*/
|
||||
getUISchema(): Record<string, unknown> {
|
||||
// Start with some top-level UI schema config (optional)
|
||||
const uiSchema: Record<string, unknown> = {
|
||||
'ui:submitButtonOptions': {
|
||||
props: {
|
||||
disabled: false,
|
||||
className: 'btn btn-info',
|
||||
},
|
||||
norender: false,
|
||||
submitText: 'Submit',
|
||||
},
|
||||
};
|
||||
|
||||
for (const [key, { schema }] of this.fieldMap.entries()) {
|
||||
let fieldUi: Record<string, unknown> = { };
|
||||
// Use the Zod description if available
|
||||
// (Accessing `._def.description` is private/hacky, but commonly done.)
|
||||
const sAny = schema as any;
|
||||
if (sAny?._def?.description) {
|
||||
fieldUi['ui:description'] = sAny._def.description;
|
||||
}
|
||||
|
||||
// RJSF usually reads 'title' from JSON schema. But if you want
|
||||
// to override it in UI schema, you can do so:
|
||||
fieldUi['ui:title'] = key[0].toUpperCase() + key.substr(1).toLowerCase()
|
||||
|
||||
// If the Zod schema allows a default, we can parse(undefined) to get it.
|
||||
try {
|
||||
const defaultVal = schema.parse(undefined);
|
||||
// There's no official 'ui:default' in RJSF, but you could do a placeholder:
|
||||
fieldUi['ui:placeholder'] = defaultVal;
|
||||
} catch {
|
||||
// no default
|
||||
}
|
||||
if(key=='path'){
|
||||
debugger
|
||||
}
|
||||
fieldUi = {
|
||||
...fieldUi,
|
||||
...this.getMetadata(key),
|
||||
}
|
||||
uiSchema[key] = fieldUi;
|
||||
}
|
||||
return uiSchema;
|
||||
}
|
||||
}
|
||||
13
packages/commons/tsconfig.json
Normal file
13
packages/commons/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../typescript-config/base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"files": ["src/index.ts"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declarationDir": "./dist",
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
|
||||
"preserveConstEnums": true
|
||||
},
|
||||
}
|
||||
12
packages/commons/tsup.config.ts
Normal file
12
packages/commons/tsup.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig((options) => ({
|
||||
entryPoints: [
|
||||
"src/*.ts"
|
||||
],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
...options,
|
||||
bundle: false
|
||||
}));
|
||||
39
packages/core/.gitignore
vendored
Normal file
39
packages/core/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
node_modules
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
44
packages/core/.kbot/completion.json
Normal file
44
packages/core/.kbot/completion.json
Normal file
@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"level": "debug",
|
||||
"message": {
|
||||
"id": "gen-1736963368-mc5na3vrtYewOp9vu7hf",
|
||||
"provider": "Amazon Bedrock",
|
||||
"model": "anthropic/claude-3.5-sonnet",
|
||||
"object": "chat.completion",
|
||||
"created": 1736963368,
|
||||
"choices": [
|
||||
{
|
||||
"logprobs": null,
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I'll help you set up a TypeScript project with tsup. I notice you have an existing TypeScript/Node.js project configuration, and you want to switch to using tsup. I'll create a new configuration that maintains your existing functionality while migrating to tsup.\n\nLet me help you set this up by:\n1. Adding tsup as a dependency\n2. Creating a tsup configuration file\n3. Updating the package.json build scripts\n\nFirst, let's install tsup:",
|
||||
"refusal": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tooluse_iLzgz83QRXm3viq24ahmpg",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"arguments": "{\"command\": \"npm\", \"args\": [\"install\", \"--save-dev\", \"tsup\"]}",
|
||||
"parsed_arguments": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"parsed": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 2592,
|
||||
"completion_tokens": 156,
|
||||
"total_tokens": 2748
|
||||
}
|
||||
},
|
||||
"timestamp": "2025-01-15T17:49:34.356Z",
|
||||
"service": "collector:onChatCompletion"
|
||||
}
|
||||
]
|
||||
8
packages/core/.kbot/content.json
Normal file
8
packages/core/.kbot/content.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"level": "debug",
|
||||
"message": "I'll help you set up a TypeScript project with tsup. I notice you have an existing TypeScript/Node.js project configuration, and you want to switch to using tsup. I'll create a new configuration that maintains your existing functionality while migrating to tsup.\n\nLet me help you set this up by:\n1. Adding tsup as a dependency\n2. Creating a tsup configuration file\n3. Updating the package.json build scripts\n\nFirst, let's install tsup:",
|
||||
"timestamp": "2025-01-15T17:49:34.361Z",
|
||||
"service": "collector:onContent"
|
||||
}
|
||||
]
|
||||
41
packages/core/.kbot/openai-message.json
Normal file
41
packages/core/.kbot/openai-message.json
Normal file
@ -0,0 +1,41 @@
|
||||
[
|
||||
{
|
||||
"level": "info",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I'll help you set up a TypeScript project with tsup. I notice you have an existing TypeScript/Node.js project configuration, and you want to switch to using tsup. I'll create a new configuration that maintains your existing functionality while migrating to tsup.\n\nLet me help you set this up by:\n1. Adding tsup as a dependency\n2. Creating a tsup configuration file\n3. Updating the package.json build scripts\n\nFirst, let's install tsup:",
|
||||
"refusal": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tooluse_iLzgz83QRXm3viq24ahmpg",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"arguments": "{\"command\": \"npm\", \"args\": [\"install\", \"--save-dev\", \"tsup\"]}",
|
||||
"parsed_arguments": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"parsed": null,
|
||||
"timestamp": "2025-01-15T17:49:34.357Z",
|
||||
"sessionId": "1736963367295",
|
||||
"prompt": "init tsup"
|
||||
},
|
||||
"timestamp": "2025-01-15T17:49:34.358Z",
|
||||
"service": "collector:onMessage"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": {
|
||||
"role": "tool",
|
||||
"tool_call_id": "tooluse_iLzgz83QRXm3viq24ahmpg",
|
||||
"content": "{\"command\":\"npm\",\"args\":[\"install\",\"--save-dev\",\"tsup\"]}",
|
||||
"timestamp": "2025-01-15T17:49:37.113Z",
|
||||
"sessionId": "1736963367295",
|
||||
"prompt": "init tsup"
|
||||
},
|
||||
"timestamp": "2025-01-15T17:49:37.115Z",
|
||||
"service": "collector:onMessage"
|
||||
}
|
||||
]
|
||||
471
packages/core/.kbot/params.json
Normal file
471
packages/core/.kbot/params.json
Normal file
@ -0,0 +1,471 @@
|
||||
{
|
||||
"model": "anthropic/claude-3.5-sonnet",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"path": "tsconfig.json",
|
||||
"content": "{\n \"extends\": \"../typescript-config/base.json\",\n \"include\": [\"src/**/*.ts\"],\n \"files\": [\"src/index.ts\"],\n \"compilerOptions\": {\n \"allowJs\": true,\n \"declarationDir\": \"./dist\",\n \"outDir\": \"./dist\",\n \"sourceMap\": true,\n \n \"preserveConstEnums\": true\n },\n}\n"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"path": "package.json",
|
||||
"content": "{\n \"name\": \"@polymech/core\",\n \"version\": \"0.2.6\",\n \"license\": \"BSD\",\n \"type\": \"module\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"licenses\": [\n {\n \"type\": \"BSD\",\n \"url\": \"https://git.osr-plastic.org/osr-plastic/osr-core/blob/master/LICENSE\"\n }\n ],\n \"exports\": {\n \"./iterator.js\": {\n \"import\": \"./dist/iterator.js\",\n \"require\": \"./dist/iterator.js\"\n },\n \"./strings.js\": {\n \"import\": \"./dist/strings.js\",\n \"require\": \"./dist/strings.js\"\n }\n },\n \"main\": \"dist/index.js\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://git.osr-plastic.org/osr-plastic/osr-core.git\"\n },\n \"types\": \"index.d.ts\",\n \"dependencies\": {\n \"tslog\": \"^3.3.3\"\n },\n \"devDependencies\": {\n \"eslint-plugin-regexp\": \"^2.7.0\",\n \"@eslint/js\": \"^9.18.0\",\n \"@repo/eslint-config\": \"workspace:*\",\n \"@repo/typescript-config\": \"workspace:*\",\n \"@types/node\": \"^8.10.66\",\n \"eslint\": \"^8.57.1\",\n \"eslint-plugin-import\": \"^2.31.0\",\n \"globals\": \"^15.14.0\",\n \"ts-node\": \"^10.9.1\",\n \"typescript\": \"^4.9.5\",\n \"typescript-eslint\": \"^8.20.0\"\n },\n \"scripts\": {\n \"test\": \"tsc && mocha build/test\",\n \"build\": \"tsc -p . --declaration\",\n \"start\": \"node build/index.js\",\n \"typings\": \"tsc -p . --declaration\",\n \"dev\": \"tsc -p . --declaration -w\"\n },\n \"modules\": [],\n \"readmeFilename\": \"Readme.md\"\n}\n"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "init tsup"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": ""
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "List all files in a directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"directory"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_files",
|
||||
"description": "Reads files in a directory with a given pattern",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"directory"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remove_file",
|
||||
"description": "Remove a file at given path",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "rename_file",
|
||||
"description": "Rename or move a file or directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": {
|
||||
"type": "string"
|
||||
},
|
||||
"dst": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "modify_project_files",
|
||||
"description": "Create or modify existing project files in one shot, preferably used for creating project structure)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "base64 encoded string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"files"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Writes to a file, given a path and content (base64). No directory or file exists check needed!",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "base64 encoded string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "file_exists",
|
||||
"description": "check if a file or folder exists",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "read a file, at given a path",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "init_repository",
|
||||
"description": "Initialize a new git repository",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "commit_files_git",
|
||||
"description": "Commit files using git",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"files"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_question",
|
||||
"description": "Ask user a simple question and get response",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Question to ask the user"
|
||||
},
|
||||
"default": {
|
||||
"type": "string",
|
||||
"description": "Default answer",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"question"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "choose_option",
|
||||
"description": "Ask user to choose from multiple options",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Message to show the user"
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of choices"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"description": "Allow multiple selections",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"choices"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_command",
|
||||
"description": "Execute a terminal command and capture output",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command to execute"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Command arguments",
|
||||
"optional": true
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Working directory for command execution",
|
||||
"optional": true
|
||||
},
|
||||
"background": {
|
||||
"type": "boolean",
|
||||
"description": "Run command in background (non-blocking)",
|
||||
"optional": true,
|
||||
"default": false
|
||||
},
|
||||
"window": {
|
||||
"type": "boolean",
|
||||
"description": "Open command in new terminal window",
|
||||
"optional": true,
|
||||
"default": false
|
||||
},
|
||||
"detached": {
|
||||
"type": "boolean",
|
||||
"description": "Run process detached from parent",
|
||||
"optional": true,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "google",
|
||||
"description": "Searches Google for the given query",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "serpapi",
|
||||
"description": "Searches Serpapi (finds locations (engine:google_local), places on the map (engine:google_maps) ) for the given query",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"default": "google"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "browse_page",
|
||||
"description": "Browse a webpage and return its content as markdown, all links, images and pages main image",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL of the webpage to browse"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "send_email",
|
||||
"description": "Sends an email",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recipient": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The email address of the recipient(s). Can be a single email or an array of emails. For \"me\", use the default email address"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "the subject",
|
||||
"optional": true
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Markdown formatted body of the email",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "capture_screen",
|
||||
"description": "Capture a screenshot and store it as file (jpg). Returns the path to the file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
"parallel_tool_calls": false
|
||||
}
|
||||
15
packages/core/.kbot/tool-call-result.json
Normal file
15
packages/core/.kbot/tool-call-result.json
Normal file
@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"level": "debug",
|
||||
"message": {
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"install",
|
||||
"--save-dev",
|
||||
"tsup"
|
||||
]
|
||||
},
|
||||
"timestamp": "2025-01-15T17:49:37.116Z",
|
||||
"service": "collector:onFunctionCallResult"
|
||||
}
|
||||
]
|
||||
22
packages/core/.kbot/tool-call.json
Normal file
22
packages/core/.kbot/tool-call.json
Normal file
@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"level": "debug",
|
||||
"message": {
|
||||
"name": "execute_command",
|
||||
"arguments": {
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"install",
|
||||
"--save-dev",
|
||||
"tsup"
|
||||
]
|
||||
},
|
||||
"parsed_arguments": null,
|
||||
"timestamp": "2025-01-15T17:49:34.359Z",
|
||||
"sessionId": "1736963367295",
|
||||
"prompt": "init tsup"
|
||||
},
|
||||
"timestamp": "2025-01-15T17:49:34.360Z",
|
||||
"service": "collector:onToolCall"
|
||||
}
|
||||
]
|
||||
5
packages/core/.npmignore
Normal file
5
packages/core/.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
src
|
||||
package-lock.json
|
||||
docs
|
||||
scripts
|
||||
29
packages/core/LICENSE
Normal file
29
packages/core/LICENSE
Normal file
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017,
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
2
packages/core/README.md
Normal file
2
packages/core/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# core
|
||||
core stuff
|
||||
95
packages/core/eslint.config.js
Normal file
95
packages/core/eslint.config.js
Normal file
@ -0,0 +1,95 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// plugins
|
||||
import regexpEslint from 'eslint-plugin-regexp';
|
||||
const typescriptEslint = tseslint.plugin;
|
||||
|
||||
// parsers
|
||||
const typescriptParser = tseslint.parser;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
files: ["src/*.{ts}"]
|
||||
},
|
||||
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
regexpEslint.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
project: ['./packages/*/tsconfig.json', './tsconfig.eslint.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
regexp: regexpEslint,
|
||||
},
|
||||
rules: {
|
||||
// These off/configured-differently-by-default rules fit well for us
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-unsafe-enum-comparison' : 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
// Todo: do we want these?
|
||||
'no-var': 'off',
|
||||
|
||||
'regexp/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/class-literal-property-style': 'off',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/only-throw-error': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/sort-type-constituents': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
// Used by Biome
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
// These rules enabled by the preset configs don't work well for us
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'prefer-const': 'off',
|
||||
|
||||
// In some cases, using explicit letter-casing is more performant than the `i` flag
|
||||
'regexp/use-ignore-case': 'off',
|
||||
'regexp/prefer-regexp-exec': 'warn',
|
||||
'regexp/prefer-regexp-test': 'warn',
|
||||
'no-control-regex': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
63
packages/core/package.json
Normal file
63
packages/core/package.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@polymech/core",
|
||||
"version": "0.2.6",
|
||||
"license": "BSD",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "BSD",
|
||||
"url": "https://git.osr-plastic.org/osr-plastic/osr-core/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"exports": {
|
||||
"./iterator": {
|
||||
"import": "./dist/iterator.js",
|
||||
"require": "./dist/iterator.cjs"
|
||||
},
|
||||
"./strings.js": {
|
||||
"import": "./dist/strings.js",
|
||||
"require": "./dist/strings.js"
|
||||
},
|
||||
"./primitives.js": {
|
||||
"import": "./dist/primitives.js",
|
||||
"require": "./dist/primitives.js"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.osr-plastic.org/osr-plastic/osr-core.git"
|
||||
},
|
||||
"types": "index.d.ts",
|
||||
"dependencies": {
|
||||
"tslog": "^3.3.3",
|
||||
"tsup": "^8.3.5",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^8.10.66",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-regexp": "^2.7.0",
|
||||
"globals": "^15.14.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tsc && mocha build/test",
|
||||
"buildtsc": "tsc -p . --declaration",
|
||||
"build": "tsup",
|
||||
"start": "node build/index.js",
|
||||
"typings": "tsc -p . --declaration",
|
||||
"dev": "tsc -p . --declaration -w"
|
||||
},
|
||||
"modules": [],
|
||||
"readmeFilename": "Readme.md"
|
||||
}
|
||||
891
packages/core/src/arrays.ts
Normal file
891
packages/core/src/arrays.ts
Normal file
@ -0,0 +1,891 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { findFirstIdxMonotonousOrArrLen } from './arraysFind.js';
|
||||
import { CancellationToken } from './cancellation.js';
|
||||
import { CancellationError } from './errors.js';
|
||||
import { ISplice } from './sequence.js';
|
||||
|
||||
/**
|
||||
* Returns the last element of an array.
|
||||
* @param array The array.
|
||||
* @param n Which element from the end (default is zero).
|
||||
*/
|
||||
export function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
|
||||
return array[array.length - (1 + n)];
|
||||
}
|
||||
|
||||
export function tail2<T>(arr: T[]): [T[], T] {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('Invalid tail call');
|
||||
}
|
||||
|
||||
return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
|
||||
}
|
||||
|
||||
export function equals<T>(one: ReadonlyArray<T> | undefined, other: ReadonlyArray<T> | undefined, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
|
||||
if (one === other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!one || !other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (one.length !== other.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0, len = one.length; i < len; i++) {
|
||||
if (!itemEquals(one[i], other[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the element at `index` by replacing it with the last element. This is faster than `splice`
|
||||
* but changes the order of the array
|
||||
*/
|
||||
export function removeFastWithoutKeepingOrder<T>(array: T[], index: number) {
|
||||
const last = array.length - 1;
|
||||
if (index < last) {
|
||||
array[index] = array[last];
|
||||
}
|
||||
array.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a binary search algorithm over a sorted array.
|
||||
*
|
||||
* @param array The array being searched.
|
||||
* @param key The value we search for.
|
||||
* @param comparator A function that takes two array elements and returns zero
|
||||
* if they are equal, a negative number if the first element precedes the
|
||||
* second one in the sorting order, or a positive number if the second element
|
||||
* precedes the first one.
|
||||
* @return See {@link binarySearch2}
|
||||
*/
|
||||
export function binarySearch<T>(array: ReadonlyArray<T>, key: T, comparator: (op1: T, op2: T) => number): number {
|
||||
return binarySearch2(array.length, i => comparator(array[i], key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a binary search algorithm over a sorted collection. Useful for cases
|
||||
* when we need to perform a binary search over something that isn't actually an
|
||||
* array, and converting data to an array would defeat the use of binary search
|
||||
* in the first place.
|
||||
*
|
||||
* @param length The collection length.
|
||||
* @param compareToKey A function that takes an index of an element in the
|
||||
* collection and returns zero if the value at this index is equal to the
|
||||
* search key, a negative number if the value precedes the search key in the
|
||||
* sorting order, or a positive number if the search key precedes the value.
|
||||
* @return A non-negative index of an element, if found. If not found, the
|
||||
* result is -(n+1) (or ~n, using bitwise notation), where n is the index
|
||||
* where the key should be inserted to maintain the sorting order.
|
||||
*/
|
||||
export function binarySearch2(length: number, compareToKey: (index: number) => number): number {
|
||||
let low = 0,
|
||||
high = length - 1;
|
||||
|
||||
while (low <= high) {
|
||||
const mid = ((low + high) / 2) | 0;
|
||||
const comp = compareToKey(mid);
|
||||
if (comp < 0) {
|
||||
low = mid + 1;
|
||||
} else if (comp > 0) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
return -(low + 1);
|
||||
}
|
||||
|
||||
type Compare<T> = (a: T, b: T) => number;
|
||||
|
||||
|
||||
export function quickSelect<T>(nth: number, data: T[], compare: Compare<T>): T {
|
||||
|
||||
nth = nth | 0;
|
||||
|
||||
if (nth >= data.length) {
|
||||
throw new TypeError('invalid index');
|
||||
}
|
||||
|
||||
const pivotValue = data[Math.floor(data.length * Math.random())];
|
||||
const lower: T[] = [];
|
||||
const higher: T[] = [];
|
||||
const pivots: T[] = [];
|
||||
|
||||
for (const value of data) {
|
||||
const val = compare(value, pivotValue);
|
||||
if (val < 0) {
|
||||
lower.push(value);
|
||||
} else if (val > 0) {
|
||||
higher.push(value);
|
||||
} else {
|
||||
pivots.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (nth < lower.length) {
|
||||
return quickSelect(nth, lower, compare);
|
||||
} else if (nth < lower.length + pivots.length) {
|
||||
return pivots[0];
|
||||
} else {
|
||||
return quickSelect(nth - (lower.length + pivots.length), higher, compare);
|
||||
}
|
||||
}
|
||||
|
||||
export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => number): T[][] {
|
||||
const result: T[][] = [];
|
||||
let currentGroup: T[] | undefined = undefined;
|
||||
for (const element of data.slice(0).sort(compare)) {
|
||||
if (!currentGroup || compare(currentGroup[0], element) !== 0) {
|
||||
currentGroup = [element];
|
||||
result.push(currentGroup);
|
||||
} else {
|
||||
currentGroup.push(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given items into a list of (non-empty) groups.
|
||||
* `shouldBeGrouped` is used to decide if two consecutive items should be in the same group.
|
||||
* The order of the items is preserved.
|
||||
*/
|
||||
export function* groupAdjacentBy<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
|
||||
let currentGroup: T[] | undefined;
|
||||
let last: T | undefined;
|
||||
for (const item of items) {
|
||||
if (last !== undefined && shouldBeGrouped(last, item)) {
|
||||
currentGroup!.push(item);
|
||||
} else {
|
||||
if (currentGroup) {
|
||||
yield currentGroup;
|
||||
}
|
||||
currentGroup = [item];
|
||||
}
|
||||
last = item;
|
||||
}
|
||||
if (currentGroup) {
|
||||
yield currentGroup;
|
||||
}
|
||||
}
|
||||
|
||||
export function forEachAdjacent<T>(arr: T[], f: (item1: T | undefined, item2: T | undefined) => void): void {
|
||||
for (let i = 0; i <= arr.length; i++) {
|
||||
f(i === 0 ? undefined : arr[i - 1], i === arr.length ? undefined : arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
export function forEachWithNeighbors<T>(arr: T[], f: (before: T | undefined, element: T, after: T | undefined) => void): void {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
f(i === 0 ? undefined : arr[i - 1], arr[i], i + 1 === arr.length ? undefined : arr[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
interface IMutableSplice<T> extends ISplice<T> {
|
||||
readonly toInsert: T[];
|
||||
deleteCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diffs two *sorted* arrays and computes the splices which apply the diff.
|
||||
*/
|
||||
export function sortedDiff<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): ISplice<T>[] {
|
||||
const result: IMutableSplice<T>[] = [];
|
||||
|
||||
function pushSplice(start: number, deleteCount: number, toInsert: T[]): void {
|
||||
if (deleteCount === 0 && toInsert.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latest = result[result.length - 1];
|
||||
|
||||
if (latest && latest.start + latest.deleteCount === start) {
|
||||
latest.deleteCount += deleteCount;
|
||||
latest.toInsert.push(...toInsert);
|
||||
} else {
|
||||
result.push({ start, deleteCount, toInsert });
|
||||
}
|
||||
}
|
||||
|
||||
let beforeIdx = 0;
|
||||
let afterIdx = 0;
|
||||
|
||||
while (true) {
|
||||
if (beforeIdx === before.length) {
|
||||
pushSplice(beforeIdx, 0, after.slice(afterIdx));
|
||||
break;
|
||||
}
|
||||
if (afterIdx === after.length) {
|
||||
pushSplice(beforeIdx, before.length - beforeIdx, []);
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeElement = before[beforeIdx];
|
||||
const afterElement = after[afterIdx];
|
||||
const n = compare(beforeElement, afterElement);
|
||||
if (n === 0) {
|
||||
// equal
|
||||
beforeIdx += 1;
|
||||
afterIdx += 1;
|
||||
} else if (n < 0) {
|
||||
// beforeElement is smaller -> before element removed
|
||||
pushSplice(beforeIdx, 1, []);
|
||||
beforeIdx += 1;
|
||||
} else if (n > 0) {
|
||||
// beforeElement is greater -> after element added
|
||||
pushSplice(beforeIdx, 0, [afterElement]);
|
||||
afterIdx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes two *sorted* arrays and computes their delta (removed, added elements).
|
||||
* Finishes in `Math.min(before.length, after.length)` steps.
|
||||
*/
|
||||
export function delta<T>(before: ReadonlyArray<T>, after: ReadonlyArray<T>, compare: (a: T, b: T) => number): { removed: T[]; added: T[] } {
|
||||
const splices = sortedDiff(before, after, compare);
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
|
||||
for (const splice of splices) {
|
||||
removed.push(...before.slice(splice.start, splice.start + splice.deleteCount));
|
||||
added.push(...splice.toInsert);
|
||||
}
|
||||
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top N elements from the array.
|
||||
*
|
||||
* Faster than sorting the entire array when the array is a lot larger than N.
|
||||
*
|
||||
* @param array The unsorted array.
|
||||
* @param compare A sort function for the elements.
|
||||
* @param n The number of elements to return.
|
||||
* @return The first n elements from array when sorted with compare.
|
||||
*/
|
||||
export function top<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, n: number): T[] {
|
||||
if (n === 0) {
|
||||
return [];
|
||||
}
|
||||
const result = array.slice(0, n).sort(compare);
|
||||
topStep(array, compare, result, n, array.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous variant of `top()` allowing for splitting up work in batches between which the event loop can run.
|
||||
*
|
||||
* Returns the top N elements from the array.
|
||||
*
|
||||
* Faster than sorting the entire array when the array is a lot larger than N.
|
||||
*
|
||||
* @param array The unsorted array.
|
||||
* @param compare A sort function for the elements.
|
||||
* @param n The number of elements to return.
|
||||
* @param batch The number of elements to examine before yielding to the event loop.
|
||||
* @return The first n elements from array when sorted with compare.
|
||||
*/
|
||||
export function topAsync<T>(array: T[], compare: (a: T, b: T) => number, n: number, batch: number, token?: CancellationToken): Promise<T[]> {
|
||||
if (n === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const o = array.length;
|
||||
const result = array.slice(0, n).sort(compare);
|
||||
for (let i = n, m = Math.min(n + batch, o); i < o; i = m, m = Math.min(m + batch, o)) {
|
||||
if (i > n) {
|
||||
await new Promise(resolve => setTimeout(resolve)); // any other delay function would starve I/O
|
||||
}
|
||||
if (token && token.isCancellationRequested) {
|
||||
throw new CancellationError();
|
||||
}
|
||||
topStep(array, compare, result, i, m);
|
||||
}
|
||||
return result;
|
||||
})()
|
||||
.then(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function topStep<T>(array: ReadonlyArray<T>, compare: (a: T, b: T) => number, result: T[], i: number, m: number): void {
|
||||
for (const n = result.length; i < m; i++) {
|
||||
const element = array[i];
|
||||
if (compare(element, result[n - 1]) < 0) {
|
||||
result.pop();
|
||||
const j = findFirstIdxMonotonousOrArrLen(result, e => compare(element, e) < 0);
|
||||
result.splice(j, 0, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns New array with all falsy values removed. The original array IS NOT modified.
|
||||
*/
|
||||
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
|
||||
return array.filter((e): e is T => !!e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all falsy values from `array`. The original array IS modified.
|
||||
*/
|
||||
export function coalesceInPlace<T>(array: Array<T | undefined | null>): asserts array is Array<T> {
|
||||
let to = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (!!array[i]) {
|
||||
array[to] = array[i];
|
||||
to += 1;
|
||||
}
|
||||
}
|
||||
array.length = to;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `Array.copyWithin` instead
|
||||
*/
|
||||
export function move(array: unknown[], from: number, to: number): void {
|
||||
array.splice(to, 0, array.splice(from, 1)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns false if the provided object is an array and not empty.
|
||||
*/
|
||||
export function isFalsyOrEmpty(obj: any): boolean {
|
||||
return !Array.isArray(obj) || obj.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the provided object is an array and has at least one element.
|
||||
*/
|
||||
export function isNonEmptyArray<T>(obj: T[] | undefined | null): obj is T[];
|
||||
export function isNonEmptyArray<T>(obj: readonly T[] | undefined | null): obj is readonly T[];
|
||||
export function isNonEmptyArray<T>(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] {
|
||||
return Array.isArray(obj) && obj.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicates from the given array. The optional keyFn allows to specify
|
||||
* how elements are checked for equality by returning an alternate value for each.
|
||||
*/
|
||||
export function distinct<T>(array: ReadonlyArray<T>, keyFn: (value: T) => unknown = value => value): T[] {
|
||||
const seen = new Set<any>();
|
||||
|
||||
return array.filter(element => {
|
||||
const key = keyFn!(element);
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function uniqueFilter<T, R>(keyFn: (t: T) => R): (t: T) => boolean {
|
||||
const seen = new Set<R>();
|
||||
|
||||
return element => {
|
||||
const key = keyFn(element);
|
||||
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(key);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function commonPrefixLength<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, equals: (a: T, b: T) => boolean = (a, b) => a === b): number {
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) {
|
||||
result++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function range(to: number): number[];
|
||||
export function range(from: number, to: number): number[];
|
||||
export function range(arg: number, to?: number): number[] {
|
||||
let from = typeof to === 'number' ? arg : 0;
|
||||
|
||||
if (typeof to === 'number') {
|
||||
from = arg;
|
||||
} else {
|
||||
from = 0;
|
||||
to = arg;
|
||||
}
|
||||
|
||||
const result: number[] = [];
|
||||
|
||||
if (from <= to) {
|
||||
for (let i = from; i < to; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
} else {
|
||||
for (let i = from; i > to; i--) {
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function index<T>(array: ReadonlyArray<T>, indexer: (t: T) => string): { [key: string]: T };
|
||||
export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper: (t: T) => R): { [key: string]: R };
|
||||
export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, mapper?: (t: T) => R): { [key: string]: R } {
|
||||
return array.reduce((r, t) => {
|
||||
r[indexer(t)] = mapper ? mapper(t) : t;
|
||||
return r;
|
||||
}, Object.create(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an element into an array. Returns a function which, when
|
||||
* called, will remove that element from the array.
|
||||
*
|
||||
* @deprecated In almost all cases, use a `Set<T>` instead.
|
||||
*/
|
||||
export function insert<T>(array: T[], element: T): () => void {
|
||||
array.push(element);
|
||||
|
||||
return () => remove(array, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an element from an array if it can be found.
|
||||
*
|
||||
* @deprecated In almost all cases, use a `Set<T>` instead.
|
||||
*/
|
||||
export function remove<T>(array: T[], element: T): T | undefined {
|
||||
const index = array.indexOf(element);
|
||||
if (index > -1) {
|
||||
array.splice(index, 1);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert `insertArr` inside `target` at `insertIndex`.
|
||||
* Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array
|
||||
*/
|
||||
export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] {
|
||||
const before = target.slice(0, insertIndex);
|
||||
const after = target.slice(insertIndex);
|
||||
return before.concat(insertArr, after);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Fisher-Yates shuffle to shuffle the given array
|
||||
*/
|
||||
export function shuffle<T>(array: T[], _seed?: number): void {
|
||||
let rand: () => number;
|
||||
|
||||
if (typeof _seed === 'number') {
|
||||
let seed = _seed;
|
||||
// Seeded random number generator in JS. Modified from:
|
||||
// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript
|
||||
rand = () => {
|
||||
const x = Math.sin(seed++) * 179426549; // throw away most significant digits and reduce any potential bias
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
} else {
|
||||
rand = Math.random;
|
||||
}
|
||||
|
||||
for (let i = array.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(rand() * (i + 1));
|
||||
const temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an element to the start of the array, if found.
|
||||
*/
|
||||
export function pushToStart<T>(arr: T[], value: T): void {
|
||||
const index = arr.indexOf(value);
|
||||
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
arr.unshift(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an element to the end of the array, if found.
|
||||
*/
|
||||
export function pushToEnd<T>(arr: T[], value: T): void {
|
||||
const index = arr.indexOf(value);
|
||||
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
arr.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function pushMany<T>(arr: T[], items: ReadonlyArray<T>): void {
|
||||
for (const item of items) {
|
||||
arr.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
export function mapArrayOrNot<T, U>(items: T | T[], fn: (_: T) => U): U | U[] {
|
||||
return Array.isArray(items) ?
|
||||
items.map(fn) :
|
||||
fn(items);
|
||||
}
|
||||
|
||||
export function asArray<T>(x: T | T[]): T[];
|
||||
export function asArray<T>(x: T | readonly T[]): readonly T[];
|
||||
export function asArray<T>(x: T | T[]): T[] {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
export function getRandomElement<T>(arr: T[]): T | undefined {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the new items in the array.
|
||||
* @param array The original array.
|
||||
* @param start The zero-based location in the array from which to start inserting elements.
|
||||
* @param newItems The items to be inserted
|
||||
*/
|
||||
export function insertInto<T>(array: T[], start: number, newItems: T[]): void {
|
||||
const startIdx = getActualStartIndex(array, start);
|
||||
const originalLength = array.length;
|
||||
const newItemsLength = newItems.length;
|
||||
array.length = originalLength + newItemsLength;
|
||||
// Move the items after the start index, start from the end so that we don't overwrite any value.
|
||||
for (let i = originalLength - 1; i >= startIdx; i--) {
|
||||
array[i + newItemsLength] = array[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < newItemsLength; i++) {
|
||||
array[i + startIdx] = newItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes elements from an array and inserts new elements in their place, returning the deleted elements. Alternative to the native Array.splice method, it
|
||||
* can only support limited number of items due to the maximum call stack size limit.
|
||||
* @param array The original array.
|
||||
* @param start The zero-based location in the array from which to start removing elements.
|
||||
* @param deleteCount The number of elements to remove.
|
||||
* @returns An array containing the elements that were deleted.
|
||||
*/
|
||||
export function splice<T>(array: T[], start: number, deleteCount: number, newItems: T[]): T[] {
|
||||
const index = getActualStartIndex(array, start);
|
||||
let result = array.splice(index, deleteCount);
|
||||
if (result === undefined) {
|
||||
// see https://bugs.webkit.org/show_bug.cgi?id=261140
|
||||
result = [];
|
||||
}
|
||||
insertInto(array, index, newItems);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the actual start index (same logic as the native splice() or slice())
|
||||
* If greater than the length of the array, start will be set to the length of the array. In this case, no element will be deleted but the method will behave as an adding function, adding as many element as item[n*] provided.
|
||||
* If negative, it will begin that many elements from the end of the array. (In this case, the origin -1, meaning -n is the index of the nth last element, and is therefore equivalent to the index of array.length - n.) If array.length + start is less than 0, it will begin from index 0.
|
||||
* @param array The target array.
|
||||
* @param start The operation index.
|
||||
*/
|
||||
function getActualStartIndex<T>(array: T[], start: number): number {
|
||||
return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* When comparing two values,
|
||||
* a negative number indicates that the first value is less than the second,
|
||||
* a positive number indicates that the first value is greater than the second,
|
||||
* and zero indicates that neither is the case.
|
||||
*/
|
||||
export type CompareResult = number;
|
||||
|
||||
export namespace CompareResult {
|
||||
export function isLessThan(result: CompareResult): boolean {
|
||||
return result < 0;
|
||||
}
|
||||
|
||||
export function isLessThanOrEqual(result: CompareResult): boolean {
|
||||
return result <= 0;
|
||||
}
|
||||
|
||||
export function isGreaterThan(result: CompareResult): boolean {
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
export function isNeitherLessOrGreaterThan(result: CompareResult): boolean {
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
export const greaterThan = 1;
|
||||
export const lessThan = -1;
|
||||
export const neitherLessOrGreaterThan = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparator `c` defines a total order `<=` on `T` as following:
|
||||
* `c(a, b) <= 0` iff `a` <= `b`.
|
||||
* We also have `c(a, b) == 0` iff `c(b, a) == 0`.
|
||||
*/
|
||||
export type Comparator<T> = (a: T, b: T) => CompareResult;
|
||||
|
||||
export function compareBy<TItem, TCompareBy>(selector: (item: TItem) => TCompareBy, comparator: Comparator<TCompareBy>): Comparator<TItem> {
|
||||
return (a, b) => comparator(selector(a), selector(b));
|
||||
}
|
||||
|
||||
export function tieBreakComparators<TItem>(...comparators: Comparator<TItem>[]): Comparator<TItem> {
|
||||
return (item1, item2) => {
|
||||
for (const comparator of comparators) {
|
||||
const result = comparator(item1, item2);
|
||||
if (!CompareResult.isNeitherLessOrGreaterThan(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return CompareResult.neitherLessOrGreaterThan;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The natural order on numbers.
|
||||
*/
|
||||
export const numberComparator: Comparator<number> = (a, b) => a - b;
|
||||
|
||||
export const booleanComparator: Comparator<boolean> = (a, b) => numberComparator(a ? 1 : 0, b ? 1 : 0);
|
||||
|
||||
export function reverseOrder<TItem>(comparator: Comparator<TItem>): Comparator<TItem> {
|
||||
return (a, b) => -comparator(a, b);
|
||||
}
|
||||
|
||||
export class ArrayQueue<T> {
|
||||
private firstIdx = 0;
|
||||
private items: T[] = [];
|
||||
private lastIdx = this.items.length - 1;
|
||||
|
||||
/**
|
||||
* Constructs a queue that is backed by the given array. Runtime is O(1).
|
||||
*/
|
||||
constructor() { }
|
||||
|
||||
get length(): number {
|
||||
return this.lastIdx - this.firstIdx + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes elements from the beginning of the queue as long as the predicate returns true.
|
||||
* If no elements were consumed, `null` is returned. Has a runtime of O(result.length).
|
||||
*/
|
||||
takeWhile(predicate: (value: T) => boolean): T[] | null {
|
||||
// P(k) := k <= this.lastIdx && predicate(this.items[k])
|
||||
// Find s := min { k | k >= this.firstIdx && !P(k) } and return this.data[this.firstIdx...s)
|
||||
|
||||
let startIdx = this.firstIdx;
|
||||
while (startIdx < this.items.length && predicate(this.items[startIdx])) {
|
||||
startIdx++;
|
||||
}
|
||||
const result = startIdx === this.firstIdx ? null : this.items.slice(this.firstIdx, startIdx);
|
||||
this.firstIdx = startIdx;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes elements from the end of the queue as long as the predicate returns true.
|
||||
* If no elements were consumed, `null` is returned.
|
||||
* The result has the same order as the underlying array!
|
||||
*/
|
||||
takeFromEndWhile(predicate: (value: T) => boolean): T[] | null {
|
||||
// P(k) := this.firstIdx >= k && predicate(this.items[k])
|
||||
// Find s := max { k | k <= this.lastIdx && !P(k) } and return this.data(s...this.lastIdx]
|
||||
|
||||
let endIdx = this.lastIdx;
|
||||
while (endIdx >= 0 && predicate(this.items[endIdx])) {
|
||||
endIdx--;
|
||||
}
|
||||
const result = endIdx === this.lastIdx ? null : this.items.slice(endIdx + 1, this.lastIdx + 1);
|
||||
this.lastIdx = endIdx;
|
||||
return result;
|
||||
}
|
||||
|
||||
peek(): T | undefined {
|
||||
if (this.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return this.items[this.firstIdx];
|
||||
}
|
||||
|
||||
peekLast(): T | undefined {
|
||||
if (this.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return this.items[this.lastIdx];
|
||||
}
|
||||
|
||||
dequeue(): T | undefined {
|
||||
const result = this.items[this.firstIdx];
|
||||
this.firstIdx++;
|
||||
return result;
|
||||
}
|
||||
|
||||
removeLast(): T | undefined {
|
||||
const result = this.items[this.lastIdx];
|
||||
this.lastIdx--;
|
||||
return result;
|
||||
}
|
||||
|
||||
takeCount(count: number): T[] {
|
||||
const result = this.items.slice(this.firstIdx, this.firstIdx + count);
|
||||
this.firstIdx += count;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is faster than an iterator and array for lazy computed data.
|
||||
*/
|
||||
export class CallbackIterable<T> {
|
||||
public static readonly empty = new CallbackIterable<never>(_callback => { });
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* Calls the callback for every item.
|
||||
* Stops when the callback returns false.
|
||||
*/
|
||||
public readonly iterate: (callback: (item: T) => boolean) => void
|
||||
) {
|
||||
}
|
||||
|
||||
forEach(handler: (item: T) => void) {
|
||||
this.iterate(item => { handler(item); return true; });
|
||||
}
|
||||
|
||||
toArray(): T[] {
|
||||
const result: T[] = [];
|
||||
this.iterate(item => { result.push(item); return true; });
|
||||
return result;
|
||||
}
|
||||
|
||||
filter(predicate: (item: T) => boolean): CallbackIterable<T> {
|
||||
return new CallbackIterable(cb => this.iterate(item => predicate(item) ? cb(item) : true));
|
||||
}
|
||||
|
||||
map<TResult>(mapFn: (item: T) => TResult): CallbackIterable<TResult> {
|
||||
return new CallbackIterable<TResult>(cb => this.iterate(item => cb(mapFn(item))));
|
||||
}
|
||||
|
||||
some(predicate: (item: T) => boolean): boolean {
|
||||
let result = false;
|
||||
this.iterate(item => { result = predicate(item); return !result; });
|
||||
return result;
|
||||
}
|
||||
|
||||
findFirst(predicate: (item: T) => boolean): T | undefined {
|
||||
let result: T | undefined;
|
||||
this.iterate(item => {
|
||||
if (predicate(item)) {
|
||||
result = item;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
findLast(predicate: (item: T) => boolean): T | undefined {
|
||||
let result: T | undefined;
|
||||
this.iterate(item => {
|
||||
if (predicate(item)) {
|
||||
result = item;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
findLastMaxBy(comparator: Comparator<T>): T | undefined {
|
||||
let result: T | undefined;
|
||||
let first = true;
|
||||
this.iterate(item => {
|
||||
if (first || CompareResult.isGreaterThan(comparator(item, result!))) {
|
||||
first = false;
|
||||
result = item;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a re-arrangement of items in an array.
|
||||
*/
|
||||
export class Permutation {
|
||||
constructor(private readonly _indexMap: readonly number[]) { }
|
||||
|
||||
/**
|
||||
* Returns a permutation that sorts the given array according to the given compare function.
|
||||
*/
|
||||
public static createSortPermutation<T>(arr: readonly T[], compareFn: (a: T, b: T) => number): Permutation {
|
||||
const sortIndices = Array.from(arr.keys()).sort((index1, index2) => compareFn(arr[index1], arr[index2]));
|
||||
return new Permutation(sortIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new array with the elements of the given array re-arranged according to this permutation.
|
||||
*/
|
||||
apply<T>(arr: readonly T[]): T[] {
|
||||
return arr.map((_, index) => arr[this._indexMap[index]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new permutation that undoes the re-arrangement of this permutation.
|
||||
*/
|
||||
inverse(): Permutation {
|
||||
const inverseIndexMap = this._indexMap.slice();
|
||||
for (let i = 0; i < this._indexMap.length; i++) {
|
||||
inverseIndexMap[this._indexMap[i]] = i;
|
||||
}
|
||||
return new Permutation(inverseIndexMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous variant of `Array.find()`, returning the first element in
|
||||
* the array for which the predicate returns true.
|
||||
*
|
||||
* This implementation does not bail early and waits for all promises to
|
||||
* resolve before returning.
|
||||
*/
|
||||
export async function findAsync<T>(array: readonly T[], predicate: (element: T, index: number) => Promise<boolean>): Promise<T | undefined> {
|
||||
const results = await Promise.all(array.map(
|
||||
async (element, index) => ({ element, ok: await predicate(element, index) })
|
||||
));
|
||||
|
||||
return results.find(r => r.ok)?.element;
|
||||
}
|
||||
202
packages/core/src/arraysFind.ts
Normal file
202
packages/core/src/arraysFind.ts
Normal file
@ -0,0 +1,202 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Comparator } from './arrays.js';
|
||||
|
||||
export function findLast<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
|
||||
const idx = findLastIdx(array, predicate);
|
||||
if (idx === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return array[idx];
|
||||
}
|
||||
|
||||
export function findLastIdx<T>(array: readonly T[], predicate: (item: T) => boolean, fromIndex = array.length - 1): number {
|
||||
for (let i = fromIndex; i >= 0; i--) {
|
||||
const element = array[i];
|
||||
|
||||
if (predicate(element)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the last item where predicate is true using binary search.
|
||||
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
|
||||
*
|
||||
* @returns `undefined` if no item matches, otherwise the last item that matches the predicate.
|
||||
*/
|
||||
export function findLastMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
|
||||
const idx = findLastIdxMonotonous(array, predicate);
|
||||
return idx === -1 ? undefined : array[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the last item where predicate is true using binary search.
|
||||
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
|
||||
*
|
||||
* @returns `startIdx - 1` if predicate is false for all items, otherwise the index of the last item that matches the predicate.
|
||||
*/
|
||||
export function findLastIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
|
||||
let i = startIdx;
|
||||
let j = endIdxEx;
|
||||
while (i < j) {
|
||||
const k = Math.floor((i + j) / 2);
|
||||
if (predicate(array[k])) {
|
||||
i = k + 1;
|
||||
} else {
|
||||
j = k;
|
||||
}
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first item where predicate is true using binary search.
|
||||
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
|
||||
*
|
||||
* @returns `undefined` if no item matches, otherwise the first item that matches the predicate.
|
||||
*/
|
||||
export function findFirstMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean): T | undefined {
|
||||
const idx = findFirstIdxMonotonousOrArrLen(array, predicate);
|
||||
return idx === array.length ? undefined : array[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first item where predicate is true using binary search.
|
||||
* `predicate` must be monotonous, i.e. `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`!
|
||||
*
|
||||
* @returns `endIdxEx` if predicate is false for all items, otherwise the index of the first item that matches the predicate.
|
||||
*/
|
||||
export function findFirstIdxMonotonousOrArrLen<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
|
||||
let i = startIdx;
|
||||
let j = endIdxEx;
|
||||
while (i < j) {
|
||||
const k = Math.floor((i + j) / 2);
|
||||
if (predicate(array[k])) {
|
||||
j = k;
|
||||
} else {
|
||||
i = k + 1;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
export function findFirstIdxMonotonous<T>(array: readonly T[], predicate: (item: T) => boolean, startIdx = 0, endIdxEx = array.length): number {
|
||||
const idx = findFirstIdxMonotonousOrArrLen(array, predicate, startIdx, endIdxEx);
|
||||
return idx === array.length ? -1 : idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this when
|
||||
* * You have a sorted array
|
||||
* * You query this array with a monotonous predicate to find the last item that has a certain property.
|
||||
* * You query this array multiple times with monotonous predicates that get weaker and weaker.
|
||||
*/
|
||||
export class MonotonousArray<T> {
|
||||
public static assertInvariants = false;
|
||||
|
||||
private _findLastMonotonousLastIdx = 0;
|
||||
private _prevFindLastPredicate: ((item: T) => boolean) | undefined;
|
||||
|
||||
constructor(private readonly _array: readonly T[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The predicate must be monotonous, i.e. `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`!
|
||||
* For subsequent calls, current predicate must be weaker than (or equal to) the previous predicate, i.e. more entries must be `true`.
|
||||
*/
|
||||
findLastMonotonous(predicate: (item: T) => boolean): T | undefined {
|
||||
if (MonotonousArray.assertInvariants) {
|
||||
if (this._prevFindLastPredicate) {
|
||||
for (const item of this._array) {
|
||||
if (this._prevFindLastPredicate(item) && !predicate(item)) {
|
||||
throw new Error('MonotonousArray: current predicate must be weaker than (or equal to) the previous predicate.');
|
||||
}
|
||||
}
|
||||
}
|
||||
this._prevFindLastPredicate = predicate;
|
||||
}
|
||||
|
||||
const idx = findLastIdxMonotonous(this._array, predicate, this._findLastMonotonousLastIdx);
|
||||
this._findLastMonotonousLastIdx = idx + 1;
|
||||
return idx === -1 ? undefined : this._array[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first item that is equal to or greater than every other item.
|
||||
*/
|
||||
export function findFirstMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
|
||||
if (array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let max = array[0];
|
||||
for (let i = 1; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
if (comparator(item, max) > 0) {
|
||||
max = item;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last item that is equal to or greater than every other item.
|
||||
*/
|
||||
export function findLastMax<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
|
||||
if (array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let max = array[0];
|
||||
for (let i = 1; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
if (comparator(item, max) >= 0) {
|
||||
max = item;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first item that is equal to or less than every other item.
|
||||
*/
|
||||
export function findFirstMin<T>(array: readonly T[], comparator: Comparator<T>): T | undefined {
|
||||
return findFirstMax(array, (a, b) => -comparator(a, b));
|
||||
}
|
||||
|
||||
export function findMaxIdx<T>(array: readonly T[], comparator: Comparator<T>): number {
|
||||
if (array.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let maxIdx = 0;
|
||||
for (let i = 1; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
if (comparator(item, array[maxIdx]) > 0) {
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
return maxIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first mapped value of the array which is not undefined.
|
||||
*/
|
||||
export function mapFindFirst<T, R>(items: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
|
||||
for (const value of items) {
|
||||
const mapped = mapFn(value);
|
||||
if (mapped !== undefined) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
348
packages/core/src/aspects.ts
Normal file
348
packages/core/src/aspects.ts
Normal file
@ -0,0 +1,348 @@
|
||||
/* -------------------------------------------------------------------------
|
||||
* aspects.ts
|
||||
*
|
||||
* A robust “aspect” system supporting:
|
||||
* - before: optionally modifies arguments (sync or async)
|
||||
* - after: optionally modifies return value (sync or async)
|
||||
* - around: complete control over function invocation
|
||||
* - error: intercept errors (sync or async)
|
||||
*
|
||||
* Works as both:
|
||||
* 1) Decorators for class methods (e.g. @before(...))
|
||||
* 2) Direct function wrappers (e.g. fn = before(fn, ...)).
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 1) SIGNALS Enum (string-based to avoid symbol issues)
|
||||
* ------------------------------------------------------------------------ */
|
||||
export enum SIGNALS {
|
||||
BEFORE = 'BEFORE',
|
||||
AFTER = 'AFTER',
|
||||
AROUND = 'AROUND',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 2) Basic Types
|
||||
* ------------------------------------------------------------------------ */
|
||||
type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
/**
|
||||
* If a function returns a Promise<T>, then its awaited type is T.
|
||||
* Otherwise, it's just ReturnType<T>.
|
||||
*/
|
||||
type AwaitedReturn<T extends AnyFunction> =
|
||||
T extends (...args: any[]) => Promise<infer U> ? U : ReturnType<T>;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 3) Advice Signatures
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* BEFORE advice:
|
||||
* - Receives `context` (the `this` of the function)
|
||||
* - Receives the original `args`
|
||||
* - Can return either nothing (`void`) or new arguments (`Parameters<T>`)
|
||||
* - Can be async, returning a Promise that resolves to new args or `void`
|
||||
*/
|
||||
export type BeforeAdvice<T extends AnyFunction> = (
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => void | Parameters<T> | Promise<void | Parameters<T>>;
|
||||
|
||||
/**
|
||||
* AFTER advice:
|
||||
* - Receives `context`, the original function’s final (awaited) result,
|
||||
* and the original arguments
|
||||
* - Can return a new result (sync or async)
|
||||
*/
|
||||
export type AfterAdvice<T extends AnyFunction> = (
|
||||
context: ThisParameterType<T>,
|
||||
result: AwaitedReturn<T>,
|
||||
args: Parameters<T>
|
||||
) => AwaitedReturn<T> | Promise<AwaitedReturn<T>>;
|
||||
|
||||
/**
|
||||
* AROUND advice:
|
||||
* - Provides a `proceed(...args)` function that calls the original method
|
||||
* - You can call `proceed` any number of times, or skip it
|
||||
* - Supports both sync and async usage
|
||||
*/
|
||||
export type AroundAdvice<T extends AnyFunction> = (
|
||||
proceed: (...args: Parameters<T>) => ReturnType<T>,
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => ReturnType<T>;
|
||||
|
||||
/**
|
||||
* ERROR advice:
|
||||
* - Intercepts errors thrown by the original method (sync or async)
|
||||
* - Can return a fallback result or rethrow
|
||||
*/
|
||||
export type ErrorAdvice<T extends AnyFunction> = (
|
||||
error: unknown,
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => ReturnType<T> | void;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 4) ISignalMap: Each signal has a distinct function wrapper signature
|
||||
* ------------------------------------------------------------------------ */
|
||||
interface ISignalMap {
|
||||
[SIGNALS.BEFORE]: <T extends AnyFunction>(original: T, advice: BeforeAdvice<T>) => T;
|
||||
[SIGNALS.AFTER]: <T extends AnyFunction>(original: T, advice: AfterAdvice<T>) => T;
|
||||
[SIGNALS.AROUND]: <T extends AnyFunction>(original: T, advice: AroundAdvice<T>) => T;
|
||||
[SIGNALS.ERROR]: <T extends AnyFunction>(original: T, advice: ErrorAdvice<T>) => T;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 5) The SignalMap Implementation
|
||||
* - This is where the actual "wrapping" logic lives.
|
||||
* ------------------------------------------------------------------------ */
|
||||
const SignalMap: ISignalMap = {
|
||||
/**
|
||||
* BEFORE:
|
||||
* - Possibly modifies arguments
|
||||
* - If returns a Promise, we await it before calling original
|
||||
* - If returns an array, we use that as new arguments
|
||||
*/
|
||||
[SIGNALS.BEFORE]<T extends AnyFunction>(original: T, advice: BeforeAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const maybeNewArgs = advice(this, args);
|
||||
|
||||
if (maybeNewArgs instanceof Promise) {
|
||||
return maybeNewArgs.then((resolvedArgs) => {
|
||||
const finalArgs = resolvedArgs || args;
|
||||
const result = original.apply(this, finalArgs);
|
||||
return (result instanceof Promise) ? result : Promise.resolve(result);
|
||||
}) as ReturnType<T>;
|
||||
} else {
|
||||
const finalArgs = Array.isArray(maybeNewArgs) ? maybeNewArgs : args;
|
||||
return original.apply(this, finalArgs);
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* AFTER:
|
||||
* - Possibly modifies the return value
|
||||
* - If original is async, we chain on its promise
|
||||
* - Advice can be sync or async
|
||||
*/
|
||||
[SIGNALS.AFTER]<T extends AnyFunction>(original: T, advice: AfterAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const result = original.apply(this, args);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return result.then((unwrapped) => {
|
||||
const maybeNewResult = advice(this, unwrapped, args);
|
||||
return (maybeNewResult instanceof Promise) ? maybeNewResult : maybeNewResult;
|
||||
}) as ReturnType<T>;
|
||||
} else {
|
||||
const maybeNewResult = advice(this, result as AwaitedReturn<T>, args);
|
||||
if (maybeNewResult instanceof Promise) {
|
||||
return maybeNewResult.then(r => r) as ReturnType<T>;
|
||||
}
|
||||
return maybeNewResult as ReturnType<T>;
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* AROUND:
|
||||
* - Full control over invocation
|
||||
* - Typically you do: proceed(...args)
|
||||
* - If you want to skip or call multiple times, you can
|
||||
*/
|
||||
[SIGNALS.AROUND]<T extends AnyFunction>(original: T, advice: AroundAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const proceed = (...innerArgs: Parameters<T>) => original.apply(this, innerArgs);
|
||||
return advice(proceed, this, args);
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* ERROR:
|
||||
* - Intercepts errors thrown by the original function or a rejected Promise
|
||||
* - Optionally returns a fallback or rethrows
|
||||
*/
|
||||
[SIGNALS.ERROR]<T extends AnyFunction>(original: T, advice: ErrorAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
if (result instanceof Promise) {
|
||||
// Handle async rejections
|
||||
return result.catch((err: unknown) => {
|
||||
return advice(err, this, args);
|
||||
}) as ReturnType<T>;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
// Synchronous error
|
||||
return advice(err, this, args) as ReturnType<T>;
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 6) Decorator Helper
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/** Checks if we’re decorating a class method. */
|
||||
function isMethod(
|
||||
_target: any,
|
||||
descriptor?: PropertyDescriptor
|
||||
): descriptor is PropertyDescriptor & { value: AnyFunction } {
|
||||
return !!descriptor && typeof descriptor.value === 'function';
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 7) Wrapped Helpers (cutMethod, cut, aspect)
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/** Strictly typed wrapping for class methods. */
|
||||
function cutMethod<T extends AnyFunction, A>(
|
||||
descriptor: PropertyDescriptor & { value: T },
|
||||
advice: A,
|
||||
type: SIGNALS
|
||||
): PropertyDescriptor {
|
||||
const original = descriptor.value;
|
||||
descriptor.value = SignalMap[type](original, advice as any); // Cast `any` or refine further
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/** Strictly typed wrapping for direct function usage. */
|
||||
function cut<T extends AnyFunction, A>(target: T, advice: A, type: SIGNALS): T {
|
||||
return SignalMap[type](target, advice as any);
|
||||
}
|
||||
|
||||
interface AspectOptions<T extends SIGNALS, A> {
|
||||
type: T;
|
||||
advice: A;
|
||||
}
|
||||
|
||||
/**
|
||||
* The core aspect(...) function
|
||||
* - Returns a decorator if used in that style
|
||||
* - Otherwise, can wrap a function directly
|
||||
*/
|
||||
function aspect<T extends SIGNALS, A>({ type, advice }: AspectOptions<T, A>) {
|
||||
// If type is invalid, produce a no-op decorator
|
||||
if (!(type in SignalMap)) {
|
||||
return function crosscut(
|
||||
target: any,
|
||||
_name?: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
) {
|
||||
return descriptor || target;
|
||||
};
|
||||
}
|
||||
|
||||
// Return a decorator function
|
||||
return function crosscut(
|
||||
target: any,
|
||||
_name?: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
): any {
|
||||
// If used on a method
|
||||
if (isMethod(target, descriptor)) {
|
||||
return cutMethod(descriptor!, advice, type);
|
||||
}
|
||||
// If used directly on a function or something else
|
||||
return cut(target, advice, type);
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 8) Overloaded Decorator/Function Wrappers
|
||||
* - Each can be used as a decorator or direct wrapper
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* `before`:
|
||||
* Decorator usage => @before((ctx, args) => ...)
|
||||
* Direct usage => myFn = before(myFn, (ctx, args) => ...)
|
||||
*/
|
||||
export function before<T extends AnyFunction>(
|
||||
advice: BeforeAdvice<T>
|
||||
): (target: any, name?: string, descriptor?: PropertyDescriptor) => any;
|
||||
export function before<T extends AnyFunction>(fn: T, advice: BeforeAdvice<T>): T;
|
||||
export function before<T extends AnyFunction>(
|
||||
arg1: T | BeforeAdvice<T>,
|
||||
arg2?: BeforeAdvice<T>
|
||||
): any {
|
||||
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
|
||||
return SignalMap[SIGNALS.BEFORE](arg1, arg2);
|
||||
}
|
||||
return aspect({ type: SIGNALS.BEFORE, advice: arg1 as BeforeAdvice<T> });
|
||||
}
|
||||
|
||||
/**
|
||||
* `after`:
|
||||
* Decorator usage => @after((ctx, result, args) => ...)
|
||||
* Direct usage => myFn = after(myFn, (ctx, result, args) => ...)
|
||||
*/
|
||||
export function after<T extends AnyFunction>(
|
||||
advice: AfterAdvice<T>
|
||||
): (target: any, name?: string, descriptor?: PropertyDescriptor) => any;
|
||||
export function after<T extends AnyFunction>(fn: T, advice: AfterAdvice<T>): T;
|
||||
export function after<T extends AnyFunction>(
|
||||
arg1: T | AfterAdvice<T>,
|
||||
arg2?: AfterAdvice<T>
|
||||
): any {
|
||||
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
|
||||
return SignalMap[SIGNALS.AFTER](arg1, arg2);
|
||||
}
|
||||
return aspect({ type: SIGNALS.AFTER, advice: arg1 as AfterAdvice<T> });
|
||||
}
|
||||
|
||||
/**
|
||||
* `around`:
|
||||
* Decorator usage => @around((proceed, ctx, args) => ...)
|
||||
* Direct usage => myFn = around(myFn, (proceed, ctx, args) => ...)
|
||||
*/
|
||||
export function around<T extends AnyFunction>(
|
||||
advice: AroundAdvice<T>
|
||||
): (target: any, name?: string, descriptor?: PropertyDescriptor) => any;
|
||||
export function around<T extends AnyFunction>(fn: T, advice: AroundAdvice<T>): T;
|
||||
export function around<T extends AnyFunction>(
|
||||
arg1: T | AroundAdvice<T>,
|
||||
arg2?: AroundAdvice<T>
|
||||
): any {
|
||||
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
|
||||
return SignalMap[SIGNALS.AROUND](arg1, arg2);
|
||||
}
|
||||
return aspect({ type: SIGNALS.AROUND, advice: arg1 as AroundAdvice<T> });
|
||||
}
|
||||
|
||||
/**
|
||||
* `error`:
|
||||
* Decorator usage => @error((err, ctx, args) => ...)
|
||||
* Direct usage => myFn = error(myFn, (err, ctx, args) => ...)
|
||||
*/
|
||||
export function error<T extends AnyFunction>(
|
||||
advice: ErrorAdvice<T>
|
||||
): (target: any, name?: string, descriptor?: PropertyDescriptor) => any;
|
||||
export function error<T extends AnyFunction>(fn: T, advice: ErrorAdvice<T>): T;
|
||||
export function error<T extends AnyFunction>(
|
||||
arg1: T | ErrorAdvice<T>,
|
||||
arg2?: ErrorAdvice<T>
|
||||
): any {
|
||||
if (typeof arg1 === 'function' && typeof arg2 === 'function') {
|
||||
return SignalMap[SIGNALS.ERROR](arg1, arg2);
|
||||
}
|
||||
return aspect({ type: SIGNALS.ERROR, advice: arg1 as ErrorAdvice<T> });
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 9) Default Export
|
||||
* ------------------------------------------------------------------------ */
|
||||
export default {
|
||||
SIGNALS,
|
||||
before,
|
||||
after,
|
||||
around,
|
||||
error,
|
||||
aspect,
|
||||
};
|
||||
232
packages/core/src/aspects_simple.ts
Normal file
232
packages/core/src/aspects_simple.ts
Normal file
@ -0,0 +1,232 @@
|
||||
/* -------------------------------------------------------------------------
|
||||
* aspects.ts
|
||||
*
|
||||
* A robust “aspect” system supporting:
|
||||
* - before: optionally modifies arguments (sync or async)
|
||||
* - after: optionally modifies return value (sync or async)
|
||||
* - around: complete control over function invocation
|
||||
* - error: intercept errors (sync or async)
|
||||
*
|
||||
* Only supports direct function wrappers (e.g. fn = before(fn, ...)).
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 1) SIGNALS Enum (string-based to avoid symbol issues)
|
||||
* ------------------------------------------------------------------------ */
|
||||
export enum SIGNALS {
|
||||
BEFORE = 'BEFORE',
|
||||
AFTER = 'AFTER',
|
||||
AROUND = 'AROUND',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 2) Basic Types
|
||||
* ------------------------------------------------------------------------ */
|
||||
type AnyFunction = (...args: any[]) => any;
|
||||
|
||||
/**
|
||||
* If a function returns a Promise<T>, then its awaited type is T.
|
||||
* Otherwise, it's just ReturnType<T>.
|
||||
*/
|
||||
type AwaitedReturn<T extends AnyFunction> =
|
||||
T extends (...args: any[]) => Promise<infer U> ? U : ReturnType<T>;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 3) Advice Signatures
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* BEFORE advice:
|
||||
* - Receives `context` (the `this` of the function)
|
||||
* - Receives the original `args`
|
||||
* - Can return either nothing (`void`) or new arguments (`Parameters<T>`)
|
||||
* - Can be async, returning a Promise that resolves to new args or `void`
|
||||
*/
|
||||
export type BeforeAdvice<T extends AnyFunction> = (
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => void | Parameters<T> | Promise<void | Parameters<T>>;
|
||||
|
||||
/**
|
||||
* AFTER advice:
|
||||
* - Receives `context`, the original function’s final (awaited) result,
|
||||
* and the original arguments
|
||||
* - Can return a new result (sync or async)
|
||||
*/
|
||||
export type AfterAdvice<T extends AnyFunction> = (
|
||||
context: ThisParameterType<T>,
|
||||
result: AwaitedReturn<T>,
|
||||
args: Parameters<T>
|
||||
) => AwaitedReturn<T> | Promise<AwaitedReturn<T>>;
|
||||
|
||||
/**
|
||||
* AROUND advice:
|
||||
* - Provides a `proceed(...args)` function that calls the original method
|
||||
* - You can call `proceed` any number of times, or skip it
|
||||
* - Supports both sync and async usage
|
||||
*/
|
||||
export type AroundAdvice<T extends AnyFunction> = (
|
||||
proceed: (...args: Parameters<T>) => ReturnType<T>,
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => ReturnType<T>;
|
||||
|
||||
/**
|
||||
* ERROR advice:
|
||||
* - Intercepts errors thrown by the original function (sync or async)
|
||||
* - Can return a fallback result or rethrow
|
||||
*/
|
||||
export type ErrorAdvice<T extends AnyFunction> = (
|
||||
error: unknown,
|
||||
context: ThisParameterType<T>,
|
||||
args: Parameters<T>
|
||||
) => ReturnType<T> | void;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 4) ISignalMap: Each signal has a distinct function wrapper signature
|
||||
* ------------------------------------------------------------------------ */
|
||||
interface ISignalMap {
|
||||
[SIGNALS.BEFORE]: <T extends AnyFunction>(original: T, advice: BeforeAdvice<T>) => T;
|
||||
[SIGNALS.AFTER]: <T extends AnyFunction>(original: T, advice: AfterAdvice<T>) => T;
|
||||
[SIGNALS.AROUND]: <T extends AnyFunction>(original: T, advice: AroundAdvice<T>) => T;
|
||||
[SIGNALS.ERROR]: <T extends AnyFunction>(original: T, advice: ErrorAdvice<T>) => T;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 5) The SignalMap Implementation
|
||||
* - This is where the actual "wrapping" logic lives.
|
||||
* ------------------------------------------------------------------------ */
|
||||
const SignalMap: ISignalMap = {
|
||||
/**
|
||||
* BEFORE:
|
||||
* - Possibly modifies arguments
|
||||
* - If returns a Promise, we await it before calling original
|
||||
* - If returns an array, we use that as new arguments
|
||||
*/
|
||||
[SIGNALS.BEFORE]<T extends AnyFunction>(original: T, advice: BeforeAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const maybeNewArgs = advice(this, args);
|
||||
|
||||
if (maybeNewArgs instanceof Promise) {
|
||||
return maybeNewArgs.then((resolvedArgs) => {
|
||||
const finalArgs = resolvedArgs || args;
|
||||
const result = original.apply(this, finalArgs);
|
||||
return (result instanceof Promise) ? result : Promise.resolve(result);
|
||||
}) as ReturnType<T>;
|
||||
} else {
|
||||
const finalArgs = Array.isArray(maybeNewArgs) ? maybeNewArgs : args;
|
||||
return original.apply(this, finalArgs);
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* AFTER:
|
||||
* - Possibly modifies the return value
|
||||
* - If original is async, we chain on its promise
|
||||
* - Advice can be sync or async
|
||||
*/
|
||||
[SIGNALS.AFTER]<T extends AnyFunction>(original: T, advice: AfterAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const result = original.apply(this, args);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return result.then((unwrapped) => {
|
||||
const maybeNewResult = advice(this, unwrapped, args);
|
||||
return (maybeNewResult instanceof Promise) ? maybeNewResult : maybeNewResult;
|
||||
}) as ReturnType<T>;
|
||||
} else {
|
||||
const maybeNewResult = advice(this, result as AwaitedReturn<T>, args);
|
||||
if (maybeNewResult instanceof Promise) {
|
||||
return maybeNewResult.then(r => r) as ReturnType<T>;
|
||||
}
|
||||
return maybeNewResult as ReturnType<T>;
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* AROUND:
|
||||
* - Full control over invocation
|
||||
* - Typically you do: proceed(...args)
|
||||
* - If you want to skip or call multiple times, you can
|
||||
*/
|
||||
[SIGNALS.AROUND]<T extends AnyFunction>(original: T, advice: AroundAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
const proceed = (...innerArgs: Parameters<T>) => original.apply(this, innerArgs);
|
||||
return advice(proceed, this, args);
|
||||
} as T;
|
||||
},
|
||||
|
||||
/**
|
||||
* ERROR:
|
||||
* - Intercepts errors thrown by the original function or a rejected Promise
|
||||
* - Optionally returns a fallback or rethrows
|
||||
*/
|
||||
[SIGNALS.ERROR]<T extends AnyFunction>(original: T, advice: ErrorAdvice<T>): T {
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
|
||||
try {
|
||||
const result = original.apply(this, args);
|
||||
if (result instanceof Promise) {
|
||||
// Handle async rejections
|
||||
return result.catch((err: unknown) => {
|
||||
return advice(err, this, args);
|
||||
}) as ReturnType<T>;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
// Synchronous error
|
||||
return advice(err, this, args) as ReturnType<T>;
|
||||
}
|
||||
} as T;
|
||||
},
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 6) Direct Usage Functions (no decorator support)
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* `before`:
|
||||
* Direct usage => myFn = before(myFn, (ctx, args) => ...)
|
||||
*/
|
||||
export function before<T extends AnyFunction>(fn: T, advice: BeforeAdvice<T>): T {
|
||||
return SignalMap[SIGNALS.BEFORE](fn, advice);
|
||||
}
|
||||
|
||||
/**
|
||||
* `after`:
|
||||
* Direct usage => myFn = after(myFn, (ctx, result, args) => ...)
|
||||
*/
|
||||
export function after<T extends AnyFunction>(fn: T, advice: AfterAdvice<T>): T {
|
||||
return SignalMap[SIGNALS.AFTER](fn, advice);
|
||||
}
|
||||
|
||||
/**
|
||||
* `around`:
|
||||
* Direct usage => myFn = around(myFn, (proceed, ctx, args) => ...)
|
||||
*/
|
||||
export function around<T extends AnyFunction>(fn: T, advice: AroundAdvice<T>): T {
|
||||
return SignalMap[SIGNALS.AROUND](fn, advice);
|
||||
}
|
||||
|
||||
/**
|
||||
* `error`:
|
||||
* Direct usage => myFn = error(myFn, (err, ctx, args) => ...)
|
||||
*/
|
||||
export function error<T extends AnyFunction>(fn: T, advice: ErrorAdvice<T>): T {
|
||||
return SignalMap[SIGNALS.ERROR](fn, advice);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 7) Default Export
|
||||
* ------------------------------------------------------------------------ */
|
||||
export default {
|
||||
SIGNALS,
|
||||
before,
|
||||
after,
|
||||
around,
|
||||
error,
|
||||
};
|
||||
|
||||
71
packages/core/src/assert.ts
Normal file
71
packages/core/src/assert.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BugIndicatingError, onUnexpectedError } from './errors.js';
|
||||
|
||||
/**
|
||||
* Throws an error with the provided message if the provided value does not evaluate to a true Javascript value.
|
||||
*
|
||||
* @deprecated Use `assert(...)` instead.
|
||||
* This method is usually used like this:
|
||||
* ```ts
|
||||
* import * as assert from 'vs/base/common/assert';
|
||||
* assert.ok(...);
|
||||
* ```
|
||||
*
|
||||
* However, `assert` in that example is a user chosen name.
|
||||
* There is no tooling for generating such an import statement.
|
||||
* Thus, the `assert(...)` function should be used instead.
|
||||
*/
|
||||
export function ok(value?: unknown, message?: string) {
|
||||
if (!value) {
|
||||
throw new Error(message ? `Assertion failed (${message})` : 'Assertion Failed');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertNever(value: never, message = 'Unreachable'): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
export function assert(condition: boolean, message = 'unexpected state'): asserts condition {
|
||||
if (!condition) {
|
||||
throw new BugIndicatingError(`Assertion Failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like assert, but doesn't throw.
|
||||
*/
|
||||
export function softAssert(condition: boolean): void {
|
||||
if (!condition) {
|
||||
onUnexpectedError(new BugIndicatingError('Soft Assertion Failed'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* condition must be side-effect free!
|
||||
*/
|
||||
export function assertFn(condition: () => boolean): void {
|
||||
if (!condition()) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
// Reevaluate `condition` again to make debugging easier
|
||||
condition();
|
||||
onUnexpectedError(new BugIndicatingError('Assertion Failed'));
|
||||
}
|
||||
}
|
||||
|
||||
export function checkAdjacentItems<T>(items: readonly T[], predicate: (item1: T, item2: T) => boolean): boolean {
|
||||
let i = 0;
|
||||
while (i < items.length - 1) {
|
||||
const a = items[i];
|
||||
const b = items[i + 1];
|
||||
if (!predicate(a, b)) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
120
packages/core/src/cache.ts
Normal file
120
packages/core/src/cache.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, CancellationTokenSource } from './cancellation.js';
|
||||
import { IDisposable } from './lifecycle.js';
|
||||
|
||||
export interface CacheResult<T> extends IDisposable {
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
export class Cache<T> {
|
||||
|
||||
private result: CacheResult<T> | null = null;
|
||||
constructor(private task: (ct: CancellationToken) => Promise<T>) { }
|
||||
|
||||
get(): CacheResult<T> {
|
||||
if (this.result) {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = this.task(cts.token);
|
||||
|
||||
this.result = {
|
||||
promise,
|
||||
dispose: () => {
|
||||
this.result = null;
|
||||
cts.cancel();
|
||||
cts.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
export function identity<T>(t: T): T {
|
||||
return t;
|
||||
}
|
||||
|
||||
interface ICacheOptions<TArg> {
|
||||
/**
|
||||
* The cache key is used to identify the cache entry.
|
||||
* Strict equality is used to compare cache keys.
|
||||
*/
|
||||
getCacheKey: (arg: TArg) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a LRU cache to make a given parametrized function cached.
|
||||
* Caches just the last key/value.
|
||||
*/
|
||||
export class LRUCachedFunction<TArg, TComputed> {
|
||||
private lastCache: TComputed | undefined = undefined;
|
||||
private lastArgKey: unknown | undefined = undefined;
|
||||
|
||||
private readonly _fn: (arg: TArg) => TComputed;
|
||||
private readonly _computeKey: (arg: TArg) => unknown;
|
||||
|
||||
constructor(fn: (arg: TArg) => TComputed);
|
||||
constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
|
||||
constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
|
||||
if (typeof arg1 === 'function') {
|
||||
this._fn = arg1;
|
||||
this._computeKey = identity;
|
||||
} else {
|
||||
this._fn = arg2!;
|
||||
this._computeKey = arg1.getCacheKey;
|
||||
}
|
||||
}
|
||||
|
||||
public get(arg: TArg): TComputed {
|
||||
const key = this._computeKey(arg);
|
||||
if (this.lastArgKey !== key) {
|
||||
this.lastArgKey = key;
|
||||
this.lastCache = this._fn(arg);
|
||||
}
|
||||
return this.lastCache!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an unbounded cache to memoize the results of the given function.
|
||||
*/
|
||||
export class CachedFunction<TArg, TComputed> {
|
||||
private readonly _map = new Map<TArg, TComputed>();
|
||||
private readonly _map2 = new Map<unknown, TComputed>();
|
||||
public get cachedValues(): ReadonlyMap<TArg, TComputed> {
|
||||
return this._map;
|
||||
}
|
||||
|
||||
private readonly _fn: (arg: TArg) => TComputed;
|
||||
private readonly _computeKey: (arg: TArg) => unknown;
|
||||
|
||||
constructor(fn: (arg: TArg) => TComputed);
|
||||
constructor(options: ICacheOptions<TArg>, fn: (arg: TArg) => TComputed);
|
||||
constructor(arg1: ICacheOptions<TArg> | ((arg: TArg) => TComputed), arg2?: (arg: TArg) => TComputed) {
|
||||
if (typeof arg1 === 'function') {
|
||||
this._fn = arg1;
|
||||
this._computeKey = identity;
|
||||
} else {
|
||||
this._fn = arg2!;
|
||||
this._computeKey = arg1.getCacheKey;
|
||||
}
|
||||
}
|
||||
|
||||
public get(arg: TArg): TComputed {
|
||||
const key = this._computeKey(arg);
|
||||
if (this._map2.has(key)) {
|
||||
return this._map2.get(key)!;
|
||||
}
|
||||
|
||||
const value = this._fn(arg);
|
||||
this._map.set(arg, value);
|
||||
this._map2.set(key, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
148
packages/core/src/cancellation.ts
Normal file
148
packages/core/src/cancellation.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from './event.js';
|
||||
import { DisposableStore, IDisposable } from './lifecycle.js';
|
||||
|
||||
export interface CancellationToken {
|
||||
|
||||
/**
|
||||
* A flag signalling is cancellation has been requested.
|
||||
*/
|
||||
readonly isCancellationRequested: boolean;
|
||||
|
||||
/**
|
||||
* An event which fires when cancellation is requested. This event
|
||||
* only ever fires `once` as cancellation can only happen once. Listeners
|
||||
* that are registered after cancellation will be called (next event loop run),
|
||||
* but also only once.
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable;
|
||||
}
|
||||
|
||||
const shortcutEvent: Event<any> = Object.freeze(function (callback, context?): IDisposable {
|
||||
const handle = setTimeout(callback.bind(context), 0);
|
||||
return { dispose() { clearTimeout(handle); } };
|
||||
});
|
||||
|
||||
export namespace CancellationToken {
|
||||
|
||||
export function isCancellationToken(thing: unknown): thing is CancellationToken {
|
||||
if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) {
|
||||
return true;
|
||||
}
|
||||
if (thing instanceof MutableToken) {
|
||||
return true;
|
||||
}
|
||||
if (!thing || typeof thing !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return typeof (thing as CancellationToken).isCancellationRequested === 'boolean'
|
||||
&& typeof (thing as CancellationToken).onCancellationRequested === 'function';
|
||||
}
|
||||
|
||||
|
||||
export const None = Object.freeze<CancellationToken>({
|
||||
isCancellationRequested: false,
|
||||
onCancellationRequested: Event.None
|
||||
});
|
||||
|
||||
export const Cancelled = Object.freeze<CancellationToken>({
|
||||
isCancellationRequested: true,
|
||||
onCancellationRequested: shortcutEvent
|
||||
});
|
||||
}
|
||||
|
||||
class MutableToken implements CancellationToken {
|
||||
|
||||
private _isCancelled: boolean = false;
|
||||
private _emitter: Emitter<any> | null = null;
|
||||
|
||||
public cancel() {
|
||||
if (!this._isCancelled) {
|
||||
this._isCancelled = true;
|
||||
if (this._emitter) {
|
||||
this._emitter.fire(undefined);
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isCancellationRequested(): boolean {
|
||||
return this._isCancelled;
|
||||
}
|
||||
|
||||
get onCancellationRequested(): Event<any> {
|
||||
if (this._isCancelled) {
|
||||
return shortcutEvent;
|
||||
}
|
||||
if (!this._emitter) {
|
||||
this._emitter = new Emitter<any>();
|
||||
}
|
||||
return this._emitter.event;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._emitter) {
|
||||
this._emitter.dispose();
|
||||
this._emitter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CancellationTokenSource {
|
||||
|
||||
private _token?: CancellationToken = undefined;
|
||||
private _parentListener?: IDisposable = undefined;
|
||||
|
||||
constructor(parent?: CancellationToken) {
|
||||
this._parentListener = parent && parent.onCancellationRequested(this.cancel, this);
|
||||
}
|
||||
|
||||
get token(): CancellationToken {
|
||||
if (!this._token) {
|
||||
// be lazy and create the token only when
|
||||
// actually needed
|
||||
this._token = new MutableToken();
|
||||
}
|
||||
return this._token;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (!this._token) {
|
||||
// save an object by returning the default
|
||||
// cancelled token when cancellation happens
|
||||
// before someone asks for the token
|
||||
this._token = CancellationToken.Cancelled;
|
||||
|
||||
} else if (this._token instanceof MutableToken) {
|
||||
// actually cancel
|
||||
this._token.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(cancel: boolean = false): void {
|
||||
if (cancel) {
|
||||
this.cancel();
|
||||
}
|
||||
this._parentListener?.dispose();
|
||||
if (!this._token) {
|
||||
// ensure to initialize with an empty token if we had none
|
||||
this._token = CancellationToken.None;
|
||||
|
||||
} else if (this._token instanceof MutableToken) {
|
||||
// actually dispose
|
||||
this._token.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelOnDispose(store: DisposableStore): CancellationToken {
|
||||
const source = new CancellationTokenSource();
|
||||
store.add({ dispose() { source.cancel(); } });
|
||||
return source.token;
|
||||
}
|
||||
422
packages/core/src/charCode.ts
Normal file
422
packages/core/src/charCode.ts
Normal file
@ -0,0 +1,422 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export const enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
|
||||
U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent
|
||||
U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent
|
||||
U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent
|
||||
U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde
|
||||
U_Combining_Macron = 0x0304, // U+0304 Combining Macron
|
||||
U_Combining_Overline = 0x0305, // U+0305 Combining Overline
|
||||
U_Combining_Breve = 0x0306, // U+0306 Combining Breve
|
||||
U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above
|
||||
U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis
|
||||
U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above
|
||||
U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above
|
||||
U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent
|
||||
U_Combining_Caron = 0x030C, // U+030C Combining Caron
|
||||
U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above
|
||||
U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above
|
||||
U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent
|
||||
U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu
|
||||
U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve
|
||||
U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above
|
||||
U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above
|
||||
U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above
|
||||
U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right
|
||||
U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below
|
||||
U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below
|
||||
U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below
|
||||
U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below
|
||||
U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above
|
||||
U_Combining_Horn = 0x031B, // U+031B Combining Horn
|
||||
U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below
|
||||
U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below
|
||||
U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below
|
||||
U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below
|
||||
U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below
|
||||
U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below
|
||||
U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below
|
||||
U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below
|
||||
U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below
|
||||
U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below
|
||||
U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below
|
||||
U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla
|
||||
U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek
|
||||
U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below
|
||||
U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below
|
||||
U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below
|
||||
U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below
|
||||
U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below
|
||||
U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below
|
||||
U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below
|
||||
U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below
|
||||
U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below
|
||||
U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line
|
||||
U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line
|
||||
U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay
|
||||
U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay
|
||||
U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay
|
||||
U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay
|
||||
U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay
|
||||
U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below
|
||||
U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below
|
||||
U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below
|
||||
U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below
|
||||
U_Combining_X_Above = 0x033D, // U+033D Combining X Above
|
||||
U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde
|
||||
U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline
|
||||
U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark
|
||||
U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark
|
||||
U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni
|
||||
U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis
|
||||
U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos
|
||||
U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni
|
||||
U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above
|
||||
U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below
|
||||
U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below
|
||||
U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below
|
||||
U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above
|
||||
U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above
|
||||
U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above
|
||||
U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below
|
||||
U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below
|
||||
U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner
|
||||
U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above
|
||||
U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above
|
||||
U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata
|
||||
U_Combining_X_Below = 0x0353, // U+0353 Combining X Below
|
||||
U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below
|
||||
U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below
|
||||
U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below
|
||||
U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above
|
||||
U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right
|
||||
U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below
|
||||
U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below
|
||||
U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above
|
||||
U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below
|
||||
U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve
|
||||
U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron
|
||||
U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below
|
||||
U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde
|
||||
U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve
|
||||
U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below
|
||||
U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A
|
||||
U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E
|
||||
U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I
|
||||
U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O
|
||||
U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U
|
||||
U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C
|
||||
U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D
|
||||
U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H
|
||||
U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M
|
||||
U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R
|
||||
U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T
|
||||
U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V
|
||||
U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X
|
||||
|
||||
/**
|
||||
* Unicode Character 'LINE SEPARATOR' (U+2028)
|
||||
* http://www.fileformat.info/info/unicode/char/2028/index.htm
|
||||
*/
|
||||
LINE_SEPARATOR_2028 = 8232,
|
||||
|
||||
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
|
||||
U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX
|
||||
U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT
|
||||
U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS
|
||||
U_MACRON = 0x00AF, // U+00AF MACRON
|
||||
U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT
|
||||
U_CEDILLA = 0x00B8, // U+00B8 CEDILLA
|
||||
U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD
|
||||
U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD
|
||||
U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD
|
||||
U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD
|
||||
U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING
|
||||
U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING
|
||||
U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK
|
||||
U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK
|
||||
U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN
|
||||
U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN
|
||||
U_BREVE = 0x02D8, // U+02D8 BREVE
|
||||
U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE
|
||||
U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE
|
||||
U_OGONEK = 0x02DB, // U+02DB OGONEK
|
||||
U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE
|
||||
U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT
|
||||
U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK
|
||||
U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT
|
||||
U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR
|
||||
U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR
|
||||
U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR
|
||||
U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR
|
||||
U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR
|
||||
U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK
|
||||
U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK
|
||||
U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED
|
||||
U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING
|
||||
U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT
|
||||
U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
|
||||
U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
|
||||
U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE
|
||||
U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON
|
||||
U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE
|
||||
U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE
|
||||
U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE
|
||||
U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE
|
||||
U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF
|
||||
U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF
|
||||
U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW
|
||||
U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN
|
||||
U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS
|
||||
U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS
|
||||
U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS
|
||||
U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI
|
||||
U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI
|
||||
U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI
|
||||
U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA
|
||||
U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA
|
||||
U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI
|
||||
U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA
|
||||
U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA
|
||||
U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI
|
||||
U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA
|
||||
U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA
|
||||
U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA
|
||||
U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA
|
||||
U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA
|
||||
|
||||
|
||||
U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE'
|
||||
|
||||
/**
|
||||
* UTF-8 BOM
|
||||
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
|
||||
* http://www.fileformat.info/info/unicode/char/feff/index.htm
|
||||
*/
|
||||
UTF8_BOM = 65279
|
||||
}
|
||||
140
packages/core/src/collections.ts
Normal file
140
packages/core/src/collections.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* An interface for a JavaScript object that
|
||||
* acts a dictionary. The keys are strings.
|
||||
*/
|
||||
export type IStringDictionary<V> = Record<string, V>;
|
||||
|
||||
/**
|
||||
* An interface for a JavaScript object that
|
||||
* acts a dictionary. The keys are numbers.
|
||||
*/
|
||||
export type INumberDictionary<V> = Record<number, V>;
|
||||
|
||||
/**
|
||||
* Groups the collection into a dictionary based on the provided
|
||||
* group function.
|
||||
*/
|
||||
export function groupBy<K extends string | number | symbol, V>(data: V[], groupFn: (element: V) => K): Record<K, V[]> {
|
||||
const result: Record<K, V[]> = Object.create(null);
|
||||
for (const element of data) {
|
||||
const key = groupFn(element);
|
||||
let target = result[key];
|
||||
if (!target) {
|
||||
target = result[key] = [];
|
||||
}
|
||||
target.push(element);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function diffSets<T>(before: ReadonlySet<T>, after: ReadonlySet<T>): { removed: T[]; added: T[] } {
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
for (const element of before) {
|
||||
if (!after.has(element)) {
|
||||
removed.push(element);
|
||||
}
|
||||
}
|
||||
for (const element of after) {
|
||||
if (!before.has(element)) {
|
||||
added.push(element);
|
||||
}
|
||||
}
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
|
||||
const removed: V[] = [];
|
||||
const added: V[] = [];
|
||||
for (const [index, value] of before) {
|
||||
if (!after.has(index)) {
|
||||
removed.push(value);
|
||||
}
|
||||
}
|
||||
for (const [index, value] of after) {
|
||||
if (!before.has(index)) {
|
||||
added.push(value);
|
||||
}
|
||||
}
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the intersection of two sets.
|
||||
*
|
||||
* @param setA - The first set.
|
||||
* @param setB - The second iterable.
|
||||
* @returns A new set containing the elements that are in both `setA` and `setB`.
|
||||
*/
|
||||
export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
|
||||
const result = new Set<T>();
|
||||
for (const elem of setB) {
|
||||
if (setA.has(elem)) {
|
||||
result.add(elem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class SetWithKey<T> implements Set<T> {
|
||||
private _map = new Map<any, T>();
|
||||
|
||||
constructor(values: T[], private toKey: (t: T) => unknown) {
|
||||
for (const value of values) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._map.size;
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
const key = this.toKey(value);
|
||||
this._map.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this._map.delete(this.toKey(value));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this._map.has(this.toKey(value));
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[T, T]> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield [entry, entry];
|
||||
}
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._map.clear();
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = 'SetWithKey';
|
||||
}
|
||||
6
packages/core/src/constants.ts
Normal file
6
packages/core/src/constants.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// standard expression for variables, eg : ${foo}
|
||||
export const REGEX_VAR = /\$\{([^\s:}]+)(?::([^\s:}]+))?\}/g
|
||||
|
||||
// alternate expression for variables, eg : %{foo}. this is required
|
||||
// to deal with parent expression parsers where '$' is reserved, eg: %{my_var}
|
||||
export const REGEX_VAR_ALT = /&\{([^\s:}]+)(?::([^\s:}]+))?\}/g
|
||||
146
packages/core/src/equals.ts
Normal file
146
packages/core/src/equals.ts
Normal file
@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arrays from './arrays.js';
|
||||
|
||||
export type EqualityComparer<T> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* Compares two items for equality using strict equality.
|
||||
*/
|
||||
export const strictEquals: EqualityComparer<any> = (a, b) => a === b;
|
||||
|
||||
/**
|
||||
* Checks if the items of two arrays are equal.
|
||||
* By default, strict equality is used to compare elements, but a custom equality comparer can be provided.
|
||||
*/
|
||||
export function itemsEquals<T>(itemEquals: EqualityComparer<T> = strictEquals): EqualityComparer<readonly T[]> {
|
||||
return (a, b) => arrays.equals(a, b, itemEquals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Two items are considered equal, if their stringified representations are equal.
|
||||
*/
|
||||
export function jsonStringifyEquals<T>(): EqualityComparer<T> {
|
||||
return (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses `item.equals(other)` to determine equality.
|
||||
*/
|
||||
export function itemEquals<T extends { equals(other: T): boolean }>(): EqualityComparer<T> {
|
||||
return (a, b) => a.equals(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two items are both null or undefined, or are equal according to the provided equality comparer.
|
||||
*/
|
||||
export function equalsIfDefined<T>(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer<T>): boolean;
|
||||
/**
|
||||
* Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer.
|
||||
*/
|
||||
export function equalsIfDefined<T>(equals: EqualityComparer<T>): EqualityComparer<T | undefined | null>;
|
||||
export function equalsIfDefined<T>(equalsOrV1: EqualityComparer<T> | T, v2?: T | undefined | null, equals?: EqualityComparer<T>): EqualityComparer<T | undefined | null> | boolean {
|
||||
if (equals !== undefined) {
|
||||
const v1 = equalsOrV1 as T | undefined;
|
||||
if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
|
||||
return v2 === v1;
|
||||
}
|
||||
return equals(v1, v2);
|
||||
} else {
|
||||
const equals = equalsOrV1 as EqualityComparer<T>;
|
||||
return (v1, v2) => {
|
||||
if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) {
|
||||
return v2 === v1;
|
||||
}
|
||||
return equals(v1, v2);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drills into arrays (items ordered) and objects (keys unordered) and uses strict equality on everything else.
|
||||
*/
|
||||
export function structuralEquals<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!structuralEquals(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a && typeof a === 'object' && b && typeof b === 'object') {
|
||||
if (Object.getPrototypeOf(a) === Object.prototype && Object.getPrototypeOf(b) === Object.prototype) {
|
||||
const aObj = a as Record<string, unknown>;
|
||||
const bObj = b as Record<string, unknown>;
|
||||
const keysA = Object.keys(aObj);
|
||||
const keysB = Object.keys(bObj);
|
||||
const keysBSet = new Set(keysB);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keysA) {
|
||||
if (!keysBSet.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (!structuralEquals(aObj[key], bObj[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `getStructuralKey(a) === getStructuralKey(b) <=> structuralEquals(a, b)`
|
||||
* (assuming that a and b are not cyclic structures and nothing extends globalThis Array).
|
||||
*/
|
||||
export function getStructuralKey(t: unknown): string {
|
||||
return JSON.stringify(toNormalizedJsonStructure(t));
|
||||
}
|
||||
|
||||
let objectId = 0;
|
||||
const objIds = new WeakMap<object, number>();
|
||||
|
||||
function toNormalizedJsonStructure(t: unknown): unknown {
|
||||
if (Array.isArray(t)) {
|
||||
return t.map(toNormalizedJsonStructure);
|
||||
}
|
||||
|
||||
if (t && typeof t === 'object') {
|
||||
if (Object.getPrototypeOf(t) === Object.prototype) {
|
||||
const tObj = t as Record<string, unknown>;
|
||||
const res: Record<string, unknown> = Object.create(null);
|
||||
for (const key of Object.keys(tObj).sort()) {
|
||||
res[key] = toNormalizedJsonStructure(tObj[key]);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
let objId = objIds.get(t);
|
||||
if (objId === undefined) {
|
||||
objId = objectId++;
|
||||
objIds.set(t, objId);
|
||||
}
|
||||
// Random string to prevent collisions
|
||||
return objId + '----2b76a038c20c4bcc';
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
326
packages/core/src/errors.ts
Normal file
326
packages/core/src/errors.ts
Normal file
@ -0,0 +1,326 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ErrorListenerCallback {
|
||||
(error: any): void;
|
||||
}
|
||||
|
||||
export interface ErrorListenerUnbind {
|
||||
(): void;
|
||||
}
|
||||
|
||||
// Avoid circular dependency on EventEmitter by implementing a subset of the interface.
|
||||
export class ErrorHandler {
|
||||
private unexpectedErrorHandler: (e: any) => void;
|
||||
private listeners: ErrorListenerCallback[];
|
||||
|
||||
constructor() {
|
||||
|
||||
this.listeners = [];
|
||||
|
||||
this.unexpectedErrorHandler = function (e: any) {
|
||||
setTimeout(() => {
|
||||
if (e.stack) {
|
||||
if (ErrorNoTelemetry.isErrorNoTelemetry(e)) {
|
||||
throw new ErrorNoTelemetry(e.message + '\n\n' + e.stack);
|
||||
}
|
||||
|
||||
throw new Error(e.message + '\n\n' + e.stack);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}, 0);
|
||||
};
|
||||
}
|
||||
|
||||
addListener(listener: ErrorListenerCallback): ErrorListenerUnbind {
|
||||
this.listeners.push(listener);
|
||||
|
||||
return () => {
|
||||
this._removeListener(listener);
|
||||
};
|
||||
}
|
||||
|
||||
private emit(e: any): void {
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(e);
|
||||
});
|
||||
}
|
||||
|
||||
private _removeListener(listener: ErrorListenerCallback): void {
|
||||
this.listeners.splice(this.listeners.indexOf(listener), 1);
|
||||
}
|
||||
|
||||
setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
|
||||
this.unexpectedErrorHandler = newUnexpectedErrorHandler;
|
||||
}
|
||||
|
||||
getUnexpectedErrorHandler(): (e: any) => void {
|
||||
return this.unexpectedErrorHandler;
|
||||
}
|
||||
|
||||
onUnexpectedError(e: any): void {
|
||||
this.unexpectedErrorHandler(e);
|
||||
this.emit(e);
|
||||
}
|
||||
|
||||
// For external errors, we don't want the listeners to be called
|
||||
onUnexpectedExternalError(e: any): void {
|
||||
this.unexpectedErrorHandler(e);
|
||||
}
|
||||
}
|
||||
|
||||
export const errorHandler = new ErrorHandler();
|
||||
|
||||
/** @skipMangle */
|
||||
export function setUnexpectedErrorHandler(newUnexpectedErrorHandler: (e: any) => void): void {
|
||||
errorHandler.setUnexpectedErrorHandler(newUnexpectedErrorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the error is a SIGPIPE error. SIGPIPE errors should generally be
|
||||
* logged at most once, to avoid a loop.
|
||||
*
|
||||
* @see https://github.com/microsoft/vscode-remote-release/issues/6481
|
||||
*/
|
||||
export function isSigPipeError(e: unknown): e is Error {
|
||||
if (!e || typeof e !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cast = e as Record<string, string | undefined>;
|
||||
return cast.code === 'EPIPE' && cast.syscall?.toUpperCase() === 'WRITE';
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should only be called with errors that indicate a bug in the product.
|
||||
* E.g. buggy extensions/invalid user-input/network issues should not be able to trigger this code path.
|
||||
* If they are, this indicates there is also a bug in the product.
|
||||
*/
|
||||
export function onBugIndicatingError(e: any): undefined {
|
||||
errorHandler.onUnexpectedError(e);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function onUnexpectedError(e: any): undefined {
|
||||
// ignore errors from cancelled promises
|
||||
if (!isCancellationError(e)) {
|
||||
errorHandler.onUnexpectedError(e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function onUnexpectedExternalError(e: any): undefined {
|
||||
// ignore errors from cancelled promises
|
||||
if (!isCancellationError(e)) {
|
||||
errorHandler.onUnexpectedExternalError(e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface SerializedError {
|
||||
readonly $isError: true;
|
||||
readonly name: string;
|
||||
readonly message: string;
|
||||
readonly stack: string;
|
||||
readonly noTelemetry: boolean;
|
||||
readonly code?: string;
|
||||
readonly cause?: SerializedError;
|
||||
}
|
||||
|
||||
type ErrorWithCode = Error & {
|
||||
code: string | undefined;
|
||||
};
|
||||
|
||||
export function transformErrorForSerialization(error: Error): SerializedError;
|
||||
export function transformErrorForSerialization(error: any): any;
|
||||
export function transformErrorForSerialization(error: any): any {
|
||||
if (error instanceof Error) {
|
||||
const { name, message, cause } = error;
|
||||
const stack: string = (<any>error).stacktrace || (<any>error).stack;
|
||||
return {
|
||||
$isError: true,
|
||||
name,
|
||||
message,
|
||||
stack,
|
||||
noTelemetry: ErrorNoTelemetry.isErrorNoTelemetry(error),
|
||||
cause: cause ? transformErrorForSerialization(cause) : undefined,
|
||||
code: (<ErrorWithCode>error).code
|
||||
};
|
||||
}
|
||||
|
||||
// return as is
|
||||
return error;
|
||||
}
|
||||
|
||||
export function transformErrorFromSerialization(data: SerializedError): Error {
|
||||
let error: Error;
|
||||
if (data.noTelemetry) {
|
||||
error = new ErrorNoTelemetry();
|
||||
} else {
|
||||
error = new Error();
|
||||
error.name = data.name;
|
||||
}
|
||||
error.message = data.message;
|
||||
error.stack = data.stack;
|
||||
if (data.code) {
|
||||
(<ErrorWithCode>error).code = data.code;
|
||||
}
|
||||
if (data.cause) {
|
||||
error.cause = transformErrorFromSerialization(data.cause);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
// see https://github.com/v8/v8/wiki/Stack%20Trace%20API#basic-stack-traces
|
||||
export interface V8CallSite {
|
||||
getThis(): unknown;
|
||||
getTypeName(): string | null;
|
||||
getFunction(): Function | undefined;
|
||||
getFunctionName(): string | null;
|
||||
getMethodName(): string | null;
|
||||
getFileName(): string | null;
|
||||
getLineNumber(): number | null;
|
||||
getColumnNumber(): number | null;
|
||||
getEvalOrigin(): string | undefined;
|
||||
isToplevel(): boolean;
|
||||
isEval(): boolean;
|
||||
isNative(): boolean;
|
||||
isConstructor(): boolean;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
const canceledName = 'Canceled';
|
||||
|
||||
/**
|
||||
* Checks if the given error is a promise in canceled state
|
||||
*/
|
||||
export function isCancellationError(error: any): boolean {
|
||||
if (error instanceof CancellationError) {
|
||||
return true;
|
||||
}
|
||||
return error instanceof Error && error.name === canceledName && error.message === canceledName;
|
||||
}
|
||||
|
||||
// !!!IMPORTANT!!!
|
||||
// Do NOT change this class because it is also used as an API-type.
|
||||
export class CancellationError extends Error {
|
||||
constructor() {
|
||||
super(canceledName);
|
||||
this.name = this.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link CancellationError `new CancellationError()`} instead
|
||||
*/
|
||||
export function canceled(): Error {
|
||||
const error = new Error(canceledName);
|
||||
error.name = error.message;
|
||||
return error;
|
||||
}
|
||||
|
||||
export function illegalArgument(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal argument: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal argument');
|
||||
}
|
||||
}
|
||||
|
||||
export function illegalState(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal state: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal state');
|
||||
}
|
||||
}
|
||||
|
||||
export class ReadonlyError extends TypeError {
|
||||
constructor(name?: string) {
|
||||
super(name ? `${name} is read-only and cannot be changed` : 'Cannot change read-only property');
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(err: any): string {
|
||||
if (!err) {
|
||||
return 'Error';
|
||||
}
|
||||
|
||||
if (err.message) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
if (err.stack) {
|
||||
return err.stack.split('\n')[0];
|
||||
}
|
||||
|
||||
return String(err);
|
||||
}
|
||||
|
||||
export class NotImplementedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super('NotImplemented');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotSupportedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super('NotSupported');
|
||||
if (message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpectedError extends Error {
|
||||
readonly isExpected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error that when thrown won't be logged in telemetry as an unhandled error.
|
||||
*/
|
||||
export class ErrorNoTelemetry extends Error {
|
||||
override readonly name: string;
|
||||
|
||||
constructor(msg?: string) {
|
||||
super(msg);
|
||||
this.name = 'CodeExpectedError';
|
||||
}
|
||||
|
||||
public static fromError(err: Error): ErrorNoTelemetry {
|
||||
if (err instanceof ErrorNoTelemetry) {
|
||||
return err;
|
||||
}
|
||||
|
||||
const result = new ErrorNoTelemetry();
|
||||
result.message = err.message;
|
||||
result.stack = err.stack;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static isErrorNoTelemetry(err: Error): err is ErrorNoTelemetry {
|
||||
return err.name === 'CodeExpectedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This error indicates a bug.
|
||||
* Do not throw this for invalid user input.
|
||||
* Only catch this error to recover gracefully from bugs.
|
||||
*/
|
||||
export class BugIndicatingError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message || 'An unexpected bug occurred.');
|
||||
Object.setPrototypeOf(this, BugIndicatingError.prototype);
|
||||
|
||||
// Because we know for sure only buggy code throws this,
|
||||
// we definitely want to break here and fix the bug.
|
||||
// debugger;
|
||||
}
|
||||
}
|
||||
1787
packages/core/src/event.ts
Normal file
1787
packages/core/src/event.ts
Normal file
File diff suppressed because it is too large
Load Diff
423
packages/core/src/extpath.ts
Normal file
423
packages/core/src/extpath.ts
Normal file
@ -0,0 +1,423 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from './charCode.js';
|
||||
import { isAbsolute, join, normalize, posix, sep } from './path.js';
|
||||
import { isWindows } from './platform.js';
|
||||
import { equalsIgnoreCase, rtrim, startsWithIgnoreCase } from './strings.js';
|
||||
import { isNumber } from './types.js';
|
||||
|
||||
export function isPathSeparator(code: number) {
|
||||
return code === CharCode.Slash || code === CharCode.Backslash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Windows OS path and changes backward slashes to forward slashes.
|
||||
* This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
|
||||
* Using it on a Linux or MaxOS path might change it.
|
||||
*/
|
||||
export function toSlashes(osPath: string) {
|
||||
return osPath.replace(/[\\/]/g, posix.sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path:
|
||||
* - turns backward slashes into forward slashes
|
||||
* - makes it absolute if it starts with a drive letter
|
||||
* This should only be done for OS paths from Windows (or user provided paths potentially from Windows).
|
||||
* Using it on a Linux or MaxOS path might change it.
|
||||
*/
|
||||
export function toPosixPath(osPath: string) {
|
||||
if (osPath.indexOf('/') === -1) {
|
||||
osPath = toSlashes(osPath);
|
||||
}
|
||||
if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { // starts with a drive letter
|
||||
osPath = '/' + osPath;
|
||||
}
|
||||
return osPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the _root_ this path, like `getRoot('c:\files') === c:\`,
|
||||
* `getRoot('files:///files/path') === files:///`,
|
||||
* or `getRoot('\\server\shares\path') === \\server\shares\`
|
||||
*/
|
||||
export function getRoot(path: string, sep: string = posix.sep): string {
|
||||
if (!path) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const len = path.length;
|
||||
const firstLetter = path.charCodeAt(0);
|
||||
if (isPathSeparator(firstLetter)) {
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// UNC candidate \\localhost\shares\ddd
|
||||
// ^^^^^^^^^^^^^^^^^^^
|
||||
if (!isPathSeparator(path.charCodeAt(2))) {
|
||||
let pos = 3;
|
||||
const start = pos;
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start !== pos && !isPathSeparator(path.charCodeAt(pos + 1))) {
|
||||
pos += 1;
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
return path.slice(0, pos + 1) // consume this separator
|
||||
.replace(/[\\/]/g, sep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /user/far
|
||||
// ^
|
||||
return sep;
|
||||
|
||||
} else if (isWindowsDriveLetter(firstLetter)) {
|
||||
// check for windows drive letter c:\ or c:
|
||||
|
||||
if (path.charCodeAt(1) === CharCode.Colon) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) {
|
||||
// C:\fff
|
||||
// ^^^
|
||||
return path.slice(0, 2) + sep;
|
||||
} else {
|
||||
// C:
|
||||
// ^^
|
||||
return path.slice(0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for URI
|
||||
// scheme://authority/path
|
||||
// ^^^^^^^^^^^^^^^^^^^
|
||||
let pos = path.indexOf('://');
|
||||
if (pos !== -1) {
|
||||
pos += 3; // 3 -> "://".length
|
||||
for (; pos < len; pos++) {
|
||||
if (isPathSeparator(path.charCodeAt(pos))) {
|
||||
return path.slice(0, pos + 1); // consume this separator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the path follows this pattern: `\\hostname\sharename`.
|
||||
*
|
||||
* @see https://msdn.microsoft.com/en-us/library/gg465305.aspx
|
||||
* @return A boolean indication if the path is a UNC path, on none-windows
|
||||
* always false.
|
||||
*/
|
||||
export function isUNC(path: string): boolean {
|
||||
if (!isWindows) {
|
||||
// UNC is a windows concept
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path || path.length < 5) {
|
||||
// at least \\a\b
|
||||
return false;
|
||||
}
|
||||
|
||||
let code = path.charCodeAt(0);
|
||||
if (code !== CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
code = path.charCodeAt(1);
|
||||
|
||||
if (code !== CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pos = 2;
|
||||
const start = pos;
|
||||
for (; pos < path.length; pos++) {
|
||||
code = path.charCodeAt(pos);
|
||||
if (code === CharCode.Backslash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start === pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
code = path.charCodeAt(pos + 1);
|
||||
|
||||
if (isNaN(code) || code === CharCode.Backslash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reference: https://en.wikipedia.org/wiki/Filename
|
||||
const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g;
|
||||
const UNIX_INVALID_FILE_CHARS = /[/]/g;
|
||||
const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i;
|
||||
export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean {
|
||||
const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS;
|
||||
|
||||
if (!name || name.length === 0 || /^\s+$/.test(name)) {
|
||||
return false; // require a name that is not just whitespace
|
||||
}
|
||||
|
||||
invalidFileChars.lastIndex = 0; // the holy grail of software development
|
||||
if (invalidFileChars.test(name)) {
|
||||
return false; // check for certain invalid file characters
|
||||
}
|
||||
|
||||
if (isWindowsOS && WINDOWS_FORBIDDEN_NAMES.test(name)) {
|
||||
return false; // check for certain invalid file names
|
||||
}
|
||||
|
||||
if (name === '.' || name === '..') {
|
||||
return false; // check for reserved values
|
||||
}
|
||||
|
||||
if (isWindowsOS && name[name.length - 1] === '.') {
|
||||
return false; // Windows: file cannot end with a "."
|
||||
}
|
||||
|
||||
if (isWindowsOS && name.length !== name.trim().length) {
|
||||
return false; // Windows: file cannot end with a whitespace
|
||||
}
|
||||
|
||||
if (name.length > 255) {
|
||||
return false; // most file systems do not allow files > 255 length
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use `IUriIdentityService.extUri.isEqual` instead. If you are
|
||||
* in a context without services, consider to pass down the `extUri` from the outside
|
||||
* or use `extUriBiasedIgnorePathCase` if you know what you are doing.
|
||||
*/
|
||||
export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boolean {
|
||||
const identityEquals = (pathA === pathB);
|
||||
if (!ignoreCase || identityEquals) {
|
||||
return identityEquals;
|
||||
}
|
||||
|
||||
if (!pathA || !pathB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return equalsIgnoreCase(pathA, pathB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated please use `IUriIdentityService.extUri.isEqualOrParent` instead. If
|
||||
* you are in a context without services, consider to pass down the `extUri` from the
|
||||
* outside, or use `extUriBiasedIgnorePathCase` if you know what you are doing.
|
||||
*/
|
||||
export function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, separator = sep): boolean {
|
||||
if (base === parentCandidate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!base || !parentCandidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.length > base.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
const beginsWith = startsWithIgnoreCase(base, parentCandidate);
|
||||
if (!beginsWith) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.length === base.length) {
|
||||
return true; // same path, different casing
|
||||
}
|
||||
|
||||
let sepOffset = parentCandidate.length;
|
||||
if (parentCandidate.charAt(parentCandidate.length - 1) === separator) {
|
||||
sepOffset--; // adjust the expected sep offset in case our candidate already ends in separator character
|
||||
}
|
||||
|
||||
return base.charAt(sepOffset) === separator;
|
||||
}
|
||||
|
||||
if (parentCandidate.charAt(parentCandidate.length - 1) !== separator) {
|
||||
parentCandidate += separator;
|
||||
}
|
||||
|
||||
return base.indexOf(parentCandidate) === 0;
|
||||
}
|
||||
|
||||
export function isWindowsDriveLetter(char0: number): boolean {
|
||||
return char0 >= CharCode.A && char0 <= CharCode.Z || char0 >= CharCode.a && char0 <= CharCode.z;
|
||||
}
|
||||
|
||||
export function sanitizeFilePath(candidate: string, cwd: string): string {
|
||||
|
||||
// Special case: allow to open a drive letter without trailing backslash
|
||||
if (isWindows && candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
// Ensure absolute
|
||||
if (!isAbsolute(candidate)) {
|
||||
candidate = join(cwd, candidate);
|
||||
}
|
||||
|
||||
// Ensure normalized
|
||||
candidate = normalize(candidate);
|
||||
|
||||
// Ensure no trailing slash/backslash
|
||||
return removeTrailingPathSeparator(candidate);
|
||||
}
|
||||
|
||||
export function removeTrailingPathSeparator(candidate: string): string {
|
||||
if (isWindows) {
|
||||
candidate = rtrim(candidate, sep);
|
||||
|
||||
// Special case: allow to open drive root ('C:\')
|
||||
if (candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
} else {
|
||||
candidate = rtrim(candidate, sep);
|
||||
|
||||
// Special case: allow to open root ('/')
|
||||
if (!candidate) {
|
||||
candidate = sep;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function isRootOrDriveLetter(path: string): boolean {
|
||||
const pathNormalized = normalize(path);
|
||||
|
||||
if (isWindows) {
|
||||
if (path.length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasDriveLetter(pathNormalized) &&
|
||||
(path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash);
|
||||
}
|
||||
|
||||
return pathNormalized === posix.sep;
|
||||
}
|
||||
|
||||
export function hasDriveLetter(path: string, isWindowsOS: boolean = isWindows): boolean {
|
||||
if (isWindowsOS) {
|
||||
return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getDriveLetter(path: string, isWindowsOS: boolean = isWindows): string | undefined {
|
||||
return hasDriveLetter(path, isWindowsOS) ? path[0] : undefined;
|
||||
}
|
||||
|
||||
export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number {
|
||||
if (candidate.length > path.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (path === candidate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ignoreCase) {
|
||||
path = path.toLowerCase();
|
||||
candidate = candidate.toLowerCase();
|
||||
}
|
||||
|
||||
return path.indexOf(candidate);
|
||||
}
|
||||
|
||||
export interface IPathWithLineAndColumn {
|
||||
path: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}
|
||||
|
||||
export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn {
|
||||
const segments = rawPath.split(':'); // C:\file.txt:<line>:<column>
|
||||
|
||||
let path: string | undefined = undefined;
|
||||
let line: number | undefined = undefined;
|
||||
let column: number | undefined = undefined;
|
||||
|
||||
for (const segment of segments) {
|
||||
const segmentAsNumber = Number(segment);
|
||||
if (!isNumber(segmentAsNumber)) {
|
||||
path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...)
|
||||
} else if (line === undefined) {
|
||||
line = segmentAsNumber;
|
||||
} else if (column === undefined) {
|
||||
column = segmentAsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`');
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
line: line !== undefined ? line : undefined,
|
||||
column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set
|
||||
};
|
||||
}
|
||||
|
||||
const pathChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const windowsSafePathFirstChars = 'BDEFGHIJKMOQRSTUVWXYZbdefghijkmoqrstuvwxyz0123456789';
|
||||
|
||||
export function randomPath(parent?: string, prefix?: string, randomLength = 8): string {
|
||||
let suffix = '';
|
||||
for (let i = 0; i < randomLength; i++) {
|
||||
let pathCharsTouse: string;
|
||||
if (i === 0 && isWindows && !prefix && (randomLength === 3 || randomLength === 4)) {
|
||||
|
||||
// Windows has certain reserved file names that cannot be used, such
|
||||
// as AUX, CON, PRN, etc. We want to avoid generating a random name
|
||||
// that matches that pattern, so we use a different set of characters
|
||||
// for the first character of the name that does not include any of
|
||||
// the reserved names first characters.
|
||||
|
||||
pathCharsTouse = windowsSafePathFirstChars;
|
||||
} else {
|
||||
pathCharsTouse = pathChars;
|
||||
}
|
||||
|
||||
suffix += pathCharsTouse.charAt(Math.floor(Math.random() * pathCharsTouse.length));
|
||||
}
|
||||
|
||||
let randomFileName: string;
|
||||
if (prefix) {
|
||||
randomFileName = `${prefix}-${suffix}`;
|
||||
} else {
|
||||
randomFileName = suffix;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
return join(parent, randomFileName);
|
||||
}
|
||||
|
||||
return randomFileName;
|
||||
}
|
||||
32
packages/core/src/functional.ts
Normal file
32
packages/core/src/functional.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Given a function, returns a function that is only calling that function once.
|
||||
*/
|
||||
export function createSingleCallFunction<T extends Function>(this: unknown, fn: T, fnDidRunCallback?: () => void): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return function () {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
|
||||
didCall = true;
|
||||
if (fnDidRunCallback) {
|
||||
try {
|
||||
result = fn.apply(_this, arguments);
|
||||
} finally {
|
||||
fnDidRunCallback();
|
||||
}
|
||||
} else {
|
||||
result = fn.apply(_this, arguments);
|
||||
}
|
||||
|
||||
return result;
|
||||
} as unknown as T;
|
||||
}
|
||||
33
packages/core/src/index.ts
Normal file
33
packages/core/src/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { substitute } from './strings.js'
|
||||
|
||||
export type Hash<T> = Record<string, T>;
|
||||
export interface List<T> {
|
||||
[index: number]: T
|
||||
length: number
|
||||
}
|
||||
/**
|
||||
* Interface of the simple literal object with any string keys.
|
||||
*/
|
||||
export type IObjectLiteral = Record<string, any>;
|
||||
|
||||
export type JSONPathExpression = string;
|
||||
|
||||
|
||||
|
||||
const _resolve = (config) => {
|
||||
for (const key in config) {
|
||||
if (config[key] && typeof config[key] == 'string') {
|
||||
const resolved = substitute(config[key], config);
|
||||
config[key] = resolved;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
export const resolveConfig = (config) => {
|
||||
config = _resolve(config);
|
||||
config = _resolve(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
export { substitute } from './strings.js'
|
||||
|
||||
262
packages/core/src/iterator.ts
Normal file
262
packages/core/src/iterator.ts
Normal file
@ -0,0 +1,262 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export namespace Iterable {
|
||||
|
||||
export function is<T = any>(thing: any): thing is Iterable<T> {
|
||||
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
const _empty: Iterable<any> = Object.freeze([]);
|
||||
export function empty<T = any>(): Iterable<T> {
|
||||
return _empty;
|
||||
}
|
||||
|
||||
export function* single<T>(element: T): Iterable<T> {
|
||||
yield element;
|
||||
}
|
||||
|
||||
export function wrap<T>(iterableOrElement: Iterable<T> | T): Iterable<T> {
|
||||
if (is(iterableOrElement)) {
|
||||
return iterableOrElement;
|
||||
} else {
|
||||
return single(iterableOrElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function from<T>(iterable: Iterable<T> | undefined | null): Iterable<T> {
|
||||
return iterable || _empty;
|
||||
}
|
||||
|
||||
export function* reverse<T>(array: Array<T>): Iterable<T> {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
yield array[i];
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
|
||||
return !iterable || iterable[Symbol.iterator]().next().done === true;
|
||||
}
|
||||
|
||||
export function first<T>(iterable: Iterable<T>): T | undefined {
|
||||
return iterable[Symbol.iterator]().next().value;
|
||||
}
|
||||
|
||||
export function some<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
|
||||
let i = 0;
|
||||
for (const element of iterable) {
|
||||
if (predicate(element, i++)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
|
||||
export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
|
||||
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
|
||||
let index = 0;
|
||||
for (const element of iterable) {
|
||||
yield fn(element, index++);
|
||||
}
|
||||
}
|
||||
|
||||
export function* flatMap<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => Iterable<R>): Iterable<R> {
|
||||
let index = 0;
|
||||
for (const element of iterable) {
|
||||
yield* fn(element, index++);
|
||||
}
|
||||
}
|
||||
|
||||
export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
|
||||
for (const iterable of iterables) {
|
||||
yield* iterable;
|
||||
}
|
||||
}
|
||||
|
||||
export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
|
||||
let value = initialValue;
|
||||
for (const element of iterable) {
|
||||
value = reducer(value, element);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
|
||||
*/
|
||||
export function* slice<T>(arr: ReadonlyArray<T>, from: number, to = arr.length): Iterable<T> {
|
||||
if (from < -arr.length) {
|
||||
from = 0;
|
||||
}
|
||||
if (from < 0) {
|
||||
from += arr.length;
|
||||
}
|
||||
|
||||
if (to < 0) {
|
||||
to += arr.length;
|
||||
} else if (to > arr.length) {
|
||||
to = arr.length;
|
||||
}
|
||||
|
||||
for (; from < to; from++) {
|
||||
yield arr[from];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes `atMost` elements from iterable and returns the consumed elements,
|
||||
* and an iterable for the rest of the elements.
|
||||
*/
|
||||
export function consume<T>(iterable: Iterable<T>, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable<T>] {
|
||||
const consumed: T[] = [];
|
||||
|
||||
if (atMost === 0) {
|
||||
return [consumed, iterable];
|
||||
}
|
||||
|
||||
const iterator = iterable[Symbol.iterator]();
|
||||
|
||||
for (let i = 0; i < atMost; i++) {
|
||||
const next = iterator.next();
|
||||
|
||||
if (next.done) {
|
||||
return [consumed, Iterable.empty()];
|
||||
}
|
||||
|
||||
consumed.push(next.value);
|
||||
}
|
||||
|
||||
return [consumed, { [Symbol.iterator]() { return iterator; } }];
|
||||
}
|
||||
|
||||
export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
||||
const result: T[] = [];
|
||||
for await (const item of iterable) {
|
||||
result.push(item);
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IIterator<T> {
|
||||
next(): T;
|
||||
}
|
||||
|
||||
export class ArrayIterator<T> implements IIterator<T> {
|
||||
|
||||
private items: T[];
|
||||
protected start: number;
|
||||
protected end: number;
|
||||
protected index: number;
|
||||
|
||||
constructor(items: T[], start: number = 0, end: number = items.length) {
|
||||
this.items = items;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.index = start - 1;
|
||||
}
|
||||
|
||||
public first(): T {
|
||||
this.index = this.start;
|
||||
return this.current();
|
||||
}
|
||||
|
||||
public next(): T {
|
||||
this.index = Math.min(this.index + 1, this.end);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
protected current(): T {
|
||||
if (this.index === this.start - 1 || this.index === this.end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.items[this.index];
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayNavigator<T> extends ArrayIterator<T> implements INavigator<T> {
|
||||
|
||||
constructor(items: T[], start: number = 0, end: number = items.length) {
|
||||
super(items, start, end);
|
||||
}
|
||||
|
||||
public current(): T {
|
||||
return super.current();
|
||||
}
|
||||
|
||||
public previous(): T {
|
||||
this.index = Math.max(this.index - 1, this.start - 1);
|
||||
return this.current();
|
||||
}
|
||||
|
||||
public first(): T {
|
||||
this.index = this.start;
|
||||
return this.current();
|
||||
}
|
||||
|
||||
public last(): T {
|
||||
this.index = this.end - 1;
|
||||
return this.current();
|
||||
}
|
||||
|
||||
public parent(): T {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MappedIterator<T, R> implements IIterator<R> {
|
||||
|
||||
constructor(protected iterator: IIterator<T>, protected fn: (item: T) => R) {
|
||||
// noop
|
||||
}
|
||||
|
||||
next() { return this.fn(this.iterator.next()); }
|
||||
}
|
||||
|
||||
export interface INavigator<T> extends IIterator<T> {
|
||||
current(): T;
|
||||
previous(): T;
|
||||
parent(): T;
|
||||
first(): T;
|
||||
last(): T;
|
||||
next(): T;
|
||||
}
|
||||
|
||||
export class MappedNavigator<T, R> extends MappedIterator<T, R> implements INavigator<R> {
|
||||
|
||||
constructor(protected navigator: INavigator<T>, fn: (item: T) => R) {
|
||||
super(navigator, fn);
|
||||
}
|
||||
|
||||
current() { return this.fn(this.navigator.current()); }
|
||||
previous() { return this.fn(this.navigator.previous()); }
|
||||
parent() { return this.fn(this.navigator.parent()); }
|
||||
first() { return this.fn(this.navigator.first()); }
|
||||
last() { return this.fn(this.navigator.last()); }
|
||||
next() { return this.fn(this.navigator.next()); }
|
||||
}
|
||||
463
packages/core/src/labels.ts
Normal file
463
packages/core/src/labels.ts
Normal file
@ -0,0 +1,463 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { hasDriveLetter, toSlashes } from './extpath.js';
|
||||
import { posix, sep, win32 } from './path.js';
|
||||
import { isMacintosh, isWindows, OperatingSystem, OS } from './platform.js';
|
||||
import { extUri, extUriIgnorePathCase } from './resources.js';
|
||||
import { rtrim, startsWithIgnoreCase } from './strings.js';
|
||||
import { URI } from './uri.js';
|
||||
|
||||
export interface IPathLabelFormatting {
|
||||
|
||||
/**
|
||||
* The OS the path label is from to produce a label
|
||||
* that matches OS expectations.
|
||||
*/
|
||||
readonly os: OperatingSystem;
|
||||
|
||||
/**
|
||||
* Whether to add a `~` when the path is in the
|
||||
* user home directory.
|
||||
*
|
||||
* Note: this only applies to Linux, macOS but not
|
||||
* Windows.
|
||||
*/
|
||||
readonly tildify?: IUserHomeProvider;
|
||||
|
||||
/**
|
||||
* Whether to convert to a relative path if the path
|
||||
* is within any of the opened workspace folders.
|
||||
*/
|
||||
readonly relative?: IRelativePathProvider;
|
||||
}
|
||||
|
||||
export interface IRelativePathProvider {
|
||||
|
||||
/**
|
||||
* Whether to not add a prefix when in multi-root workspace.
|
||||
*/
|
||||
readonly noPrefix?: boolean;
|
||||
|
||||
getWorkspace(): { folders: { uri: URI; name?: string }[] };
|
||||
getWorkspaceFolder(resource: URI): { uri: URI; name?: string } | null;
|
||||
}
|
||||
|
||||
export interface IUserHomeProvider {
|
||||
userHome: URI;
|
||||
}
|
||||
|
||||
export function getPathLabel(resource: URI, formatting: IPathLabelFormatting): string {
|
||||
const { os, tildify: tildifier, relative: relatifier } = formatting;
|
||||
|
||||
// return early with a relative path if we can resolve one
|
||||
if (relatifier) {
|
||||
const relativePath = getRelativePathLabel(resource, relatifier, os);
|
||||
if (typeof relativePath === 'string') {
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise try to resolve a absolute path label and
|
||||
// apply target OS standard path separators if target
|
||||
// OS differs from actual OS we are running in
|
||||
let absolutePath = resource.fsPath;
|
||||
if (os === OperatingSystem.Windows && !isWindows) {
|
||||
absolutePath = absolutePath.replace(/\//g, '\\');
|
||||
} else if (os !== OperatingSystem.Windows && isWindows) {
|
||||
absolutePath = absolutePath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
// macOS/Linux: tildify with provided user home directory
|
||||
if (os !== OperatingSystem.Windows && tildifier?.userHome) {
|
||||
const userHome = tildifier.userHome.fsPath;
|
||||
|
||||
// This is a bit of a hack, but in order to figure out if the
|
||||
// resource is in the user home, we need to make sure to convert it
|
||||
// to a user home resource. We cannot assume that the resource is
|
||||
// already a user home resource.
|
||||
let userHomeCandidate: string;
|
||||
if (resource.scheme !== tildifier.userHome.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) {
|
||||
userHomeCandidate = tildifier.userHome.with({ path: resource.path }).fsPath;
|
||||
} else {
|
||||
userHomeCandidate = absolutePath;
|
||||
}
|
||||
|
||||
absolutePath = tildify(userHomeCandidate, userHome, os);
|
||||
}
|
||||
|
||||
// normalize
|
||||
const pathLib = os === OperatingSystem.Windows ? win32 : posix;
|
||||
return pathLib.normalize(normalizeDriveLetter(absolutePath, os === OperatingSystem.Windows));
|
||||
}
|
||||
|
||||
function getRelativePathLabel(resource: URI, relativePathProvider: IRelativePathProvider, os: OperatingSystem): string | undefined {
|
||||
const pathLib = os === OperatingSystem.Windows ? win32 : posix;
|
||||
const extUriLib = os === OperatingSystem.Linux ? extUri : extUriIgnorePathCase;
|
||||
|
||||
const workspace = relativePathProvider.getWorkspace();
|
||||
const firstFolder = workspace.folders.at(0);
|
||||
if (!firstFolder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// This is a bit of a hack, but in order to figure out the folder
|
||||
// the resource belongs to, we need to make sure to convert it
|
||||
// to a workspace resource. We cannot assume that the resource is
|
||||
// already matching the workspace.
|
||||
if (resource.scheme !== firstFolder.uri.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) {
|
||||
resource = firstFolder.uri.with({ path: resource.path });
|
||||
}
|
||||
|
||||
const folder = relativePathProvider.getWorkspaceFolder(resource);
|
||||
if (!folder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let relativePathLabel: string | undefined = undefined;
|
||||
if (extUriLib.isEqual(folder.uri, resource)) {
|
||||
relativePathLabel = ''; // no label if paths are identical
|
||||
} else {
|
||||
relativePathLabel = extUriLib.relativePath(folder.uri, resource) ?? '';
|
||||
}
|
||||
|
||||
// normalize
|
||||
if (relativePathLabel) {
|
||||
relativePathLabel = pathLib.normalize(relativePathLabel);
|
||||
}
|
||||
|
||||
// always show root basename if there are multiple folders
|
||||
if (workspace.folders.length > 1 && !relativePathProvider.noPrefix) {
|
||||
const rootName = folder.name ? folder.name : extUriLib.basenameOrAuthority(folder.uri);
|
||||
relativePathLabel = relativePathLabel ? `${rootName} • ${relativePathLabel}` : rootName;
|
||||
}
|
||||
|
||||
return relativePathLabel;
|
||||
}
|
||||
|
||||
export function normalizeDriveLetter(path: string, isWindowsOS: boolean = isWindows): string {
|
||||
if (hasDriveLetter(path, isWindowsOS)) {
|
||||
return path.charAt(0).toUpperCase() + path.slice(1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null);
|
||||
export function tildify(path: string, userHome: string, os = OS): string {
|
||||
if (os === OperatingSystem.Windows || !path || !userHome) {
|
||||
return path; // unsupported on Windows
|
||||
}
|
||||
|
||||
let normalizedUserHome = normalizedUserHomeCached.original === userHome ? normalizedUserHomeCached.normalized : undefined;
|
||||
if (!normalizedUserHome) {
|
||||
normalizedUserHome = userHome;
|
||||
if (isWindows) {
|
||||
normalizedUserHome = toSlashes(normalizedUserHome); // make sure that the path is POSIX normalized on Windows
|
||||
}
|
||||
normalizedUserHome = `${rtrim(normalizedUserHome, posix.sep)}${posix.sep}`;
|
||||
normalizedUserHomeCached = { original: userHome, normalized: normalizedUserHome };
|
||||
}
|
||||
|
||||
let normalizedPath = path;
|
||||
if (isWindows) {
|
||||
normalizedPath = toSlashes(normalizedPath); // make sure that the path is POSIX normalized on Windows
|
||||
}
|
||||
|
||||
// Linux: case sensitive, macOS: case insensitive
|
||||
if (os === OperatingSystem.Linux ? normalizedPath.startsWith(normalizedUserHome) : startsWithIgnoreCase(normalizedPath, normalizedUserHome)) {
|
||||
return `~/${normalizedPath.substr(normalizedUserHome.length)}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export function untildify(path: string, userHome: string): string {
|
||||
return path.replace(/^~($|\/|\\)/, `${userHome}$1`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens the paths but keeps them easy to distinguish.
|
||||
* Replaces not important parts with ellipsis.
|
||||
* Every shorten path matches only one original path and vice versa.
|
||||
*
|
||||
* Algorithm for shortening paths is as follows:
|
||||
* 1. For every path in list, find unique substring of that path.
|
||||
* 2. Unique substring along with ellipsis is shortened path of that path.
|
||||
* 3. To find unique substring of path, consider every segment of length from 1 to path.length of path from end of string
|
||||
* and if present segment is not substring to any other paths then present segment is unique path,
|
||||
* else check if it is not present as suffix of any other path and present segment is suffix of path itself,
|
||||
* if it is true take present segment as unique path.
|
||||
* 4. Apply ellipsis to unique segment according to whether segment is present at start/in-between/end of path.
|
||||
*
|
||||
* Example 1
|
||||
* 1. consider 2 paths i.e. ['a\\b\\c\\d', 'a\\f\\b\\c\\d']
|
||||
* 2. find unique path of first path,
|
||||
* a. 'd' is present in path2 and is suffix of path2, hence not unique of present path.
|
||||
* b. 'c' is present in path2 and 'c' is not suffix of present path, similarly for 'b' and 'a' also.
|
||||
* c. 'd\\c' is suffix of path2.
|
||||
* d. 'b\\c' is not suffix of present path.
|
||||
* e. 'a\\b' is not present in path2, hence unique path is 'a\\b...'.
|
||||
* 3. for path2, 'f' is not present in path1 hence unique is '...\\f\\...'.
|
||||
*
|
||||
* Example 2
|
||||
* 1. consider 2 paths i.e. ['a\\b', 'a\\b\\c'].
|
||||
* a. Even if 'b' is present in path2, as 'b' is suffix of path1 and is not suffix of path2, unique path will be '...\\b'.
|
||||
* 2. for path2, 'c' is not present in path1 hence unique path is '..\\c'.
|
||||
*/
|
||||
const ellipsis = '\u2026';
|
||||
const unc = '\\\\';
|
||||
const home = '~';
|
||||
export function shorten(paths: string[], pathSeparator: string = sep): string[] {
|
||||
const shortenedPaths: string[] = new Array(paths.length);
|
||||
|
||||
// for every path
|
||||
let match = false;
|
||||
for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
|
||||
const originalPath = paths[pathIndex];
|
||||
|
||||
if (originalPath === '') {
|
||||
shortenedPaths[pathIndex] = `.${pathSeparator}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!originalPath) {
|
||||
shortenedPaths[pathIndex] = originalPath;
|
||||
continue;
|
||||
}
|
||||
|
||||
match = true;
|
||||
|
||||
// trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later
|
||||
let prefix = '';
|
||||
let trimmedPath = originalPath;
|
||||
if (trimmedPath.indexOf(unc) === 0) {
|
||||
prefix = trimmedPath.substr(0, trimmedPath.indexOf(unc) + unc.length);
|
||||
trimmedPath = trimmedPath.substr(trimmedPath.indexOf(unc) + unc.length);
|
||||
} else if (trimmedPath.indexOf(pathSeparator) === 0) {
|
||||
prefix = trimmedPath.substr(0, trimmedPath.indexOf(pathSeparator) + pathSeparator.length);
|
||||
trimmedPath = trimmedPath.substr(trimmedPath.indexOf(pathSeparator) + pathSeparator.length);
|
||||
} else if (trimmedPath.indexOf(home) === 0) {
|
||||
prefix = trimmedPath.substr(0, trimmedPath.indexOf(home) + home.length);
|
||||
trimmedPath = trimmedPath.substr(trimmedPath.indexOf(home) + home.length);
|
||||
}
|
||||
|
||||
// pick the first shortest subpath found
|
||||
const segments: string[] = trimmedPath.split(pathSeparator);
|
||||
for (let subpathLength = 1; match && subpathLength <= segments.length; subpathLength++) {
|
||||
for (let start = segments.length - subpathLength; match && start >= 0; start--) {
|
||||
match = false;
|
||||
let subpath = segments.slice(start, start + subpathLength).join(pathSeparator);
|
||||
|
||||
// that is unique to any other path
|
||||
for (let otherPathIndex = 0; !match && otherPathIndex < paths.length; otherPathIndex++) {
|
||||
|
||||
// suffix subpath treated specially as we consider no match 'x' and 'x/...'
|
||||
if (otherPathIndex !== pathIndex && paths[otherPathIndex] && paths[otherPathIndex].indexOf(subpath) > -1) {
|
||||
const isSubpathEnding: boolean = (start + subpathLength === segments.length);
|
||||
|
||||
// Adding separator as prefix for subpath, such that 'endsWith(src, trgt)' considers subpath as directory name instead of plain string.
|
||||
// prefix is not added when either subpath is root directory or path[otherPathIndex] does not have multiple directories.
|
||||
const subpathWithSep: string = (start > 0 && paths[otherPathIndex].indexOf(pathSeparator) > -1) ? pathSeparator + subpath : subpath;
|
||||
const isOtherPathEnding: boolean = paths[otherPathIndex].endsWith(subpathWithSep);
|
||||
|
||||
match = !isSubpathEnding || isOtherPathEnding;
|
||||
}
|
||||
}
|
||||
|
||||
// found unique subpath
|
||||
if (!match) {
|
||||
let result = '';
|
||||
|
||||
// preserve disk drive or root prefix
|
||||
if (segments[0].endsWith(':') || prefix !== '') {
|
||||
if (start === 1) {
|
||||
// extend subpath to include disk drive prefix
|
||||
start = 0;
|
||||
subpathLength++;
|
||||
subpath = segments[0] + pathSeparator + subpath;
|
||||
}
|
||||
|
||||
if (start > 0) {
|
||||
result = segments[0] + pathSeparator;
|
||||
}
|
||||
|
||||
result = prefix + result;
|
||||
}
|
||||
|
||||
// add ellipsis at the beginning if needed
|
||||
if (start > 0) {
|
||||
result = result + ellipsis + pathSeparator;
|
||||
}
|
||||
|
||||
result = result + subpath;
|
||||
|
||||
// add ellipsis at the end if needed
|
||||
if (start + subpathLength < segments.length) {
|
||||
result = result + pathSeparator + ellipsis;
|
||||
}
|
||||
|
||||
shortenedPaths[pathIndex] = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
shortenedPaths[pathIndex] = originalPath; // use original path if no unique subpaths found
|
||||
}
|
||||
}
|
||||
|
||||
return shortenedPaths;
|
||||
}
|
||||
|
||||
export interface ISeparator {
|
||||
label: string;
|
||||
}
|
||||
|
||||
enum Type {
|
||||
TEXT,
|
||||
VARIABLE,
|
||||
SEPARATOR
|
||||
}
|
||||
|
||||
interface ISegment {
|
||||
value: string;
|
||||
type: Type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to insert values for specific template variables into the string. E.g. "this $(is) a $(template)" can be
|
||||
* passed to this function together with an object that maps "is" and "template" to strings to have them replaced.
|
||||
* @param value string to which template is applied
|
||||
* @param values the values of the templates to use
|
||||
*/
|
||||
export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string {
|
||||
const segments: ISegment[] = [];
|
||||
|
||||
let inVariable = false;
|
||||
let curVal = '';
|
||||
for (const char of template) {
|
||||
// Beginning of variable
|
||||
if (char === '$' || (inVariable && char === '{')) {
|
||||
if (curVal) {
|
||||
segments.push({ value: curVal, type: Type.TEXT });
|
||||
}
|
||||
|
||||
curVal = '';
|
||||
inVariable = true;
|
||||
}
|
||||
|
||||
// End of variable
|
||||
else if (char === '}' && inVariable) {
|
||||
const resolved = values[curVal];
|
||||
|
||||
// Variable
|
||||
if (typeof resolved === 'string') {
|
||||
if (resolved.length) {
|
||||
segments.push({ value: resolved, type: Type.VARIABLE });
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
else if (resolved) {
|
||||
const prevSegment = segments[segments.length - 1];
|
||||
if (!prevSegment || prevSegment.type !== Type.SEPARATOR) {
|
||||
segments.push({ value: resolved.label, type: Type.SEPARATOR }); // prevent duplicate separators
|
||||
}
|
||||
}
|
||||
|
||||
curVal = '';
|
||||
inVariable = false;
|
||||
}
|
||||
|
||||
// Text or Variable Name
|
||||
else {
|
||||
curVal += char;
|
||||
}
|
||||
}
|
||||
|
||||
// Tail
|
||||
if (curVal && !inVariable) {
|
||||
segments.push({ value: curVal, type: Type.TEXT });
|
||||
}
|
||||
|
||||
return segments.filter((segment, index) => {
|
||||
|
||||
// Only keep separator if we have values to the left and right
|
||||
if (segment.type === Type.SEPARATOR) {
|
||||
const left = segments[index - 1];
|
||||
const right = segments[index + 1];
|
||||
|
||||
return [left, right].every(segment => segment && (segment.type === Type.VARIABLE || segment.type === Type.TEXT) && segment.value.length > 0);
|
||||
}
|
||||
|
||||
// accept any TEXT and VARIABLE
|
||||
return true;
|
||||
}).map(segment => segment.value).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mnemonics for menu items. Depending on OS:
|
||||
* - Windows: Supported via & character (replace && with &)
|
||||
* - Linux: Supported via & character (replace && with &)
|
||||
* - macOS: Unsupported (replace && with empty string)
|
||||
*/
|
||||
export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean): string {
|
||||
if (isMacintosh || forceDisableMnemonics) {
|
||||
return label.replace(/\(&&\w\)|&&/g, '').replace(/&/g, isMacintosh ? '&' : '&&');
|
||||
}
|
||||
|
||||
return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mnemonics for buttons. Depending on OS:
|
||||
* - Windows: Supported via & character (replace && with & and & with && for escaping)
|
||||
* - Linux: Supported via _ character (replace && with _)
|
||||
* - macOS: Unsupported (replace && with empty string)
|
||||
*/
|
||||
export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): string {
|
||||
if (isMacintosh || forceDisableMnemonics) {
|
||||
return label.replace(/\(&&\w\)|&&/g, '');
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&');
|
||||
}
|
||||
|
||||
return label.replace(/&&/g, '_');
|
||||
}
|
||||
|
||||
export function unmnemonicLabel(label: string): string {
|
||||
return label.replace(/&/g, '&&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes.
|
||||
* If the location is remote, the remote name is included in the name part.
|
||||
*/
|
||||
export function splitRecentLabel(recentLabel: string): { name: string; parentPath: string } {
|
||||
if (recentLabel.endsWith(']')) {
|
||||
// label with workspace suffix
|
||||
const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2);
|
||||
if (lastIndexOfSquareBracket !== -1) {
|
||||
const split = splitName(recentLabel.substring(0, lastIndexOfSquareBracket));
|
||||
const remoteNameWithSpace = recentLabel.substring(lastIndexOfSquareBracket);
|
||||
return { name: split.name + remoteNameWithSpace, parentPath: split.parentPath };
|
||||
}
|
||||
}
|
||||
return splitName(recentLabel);
|
||||
}
|
||||
|
||||
function splitName(fullPath: string): { name: string; parentPath: string } {
|
||||
const p = fullPath.indexOf('/') !== -1 ? posix : win32;
|
||||
const name = p.basename(fullPath);
|
||||
const parentPath = p.dirname(fullPath);
|
||||
if (name.length) {
|
||||
return { name, parentPath };
|
||||
}
|
||||
// only the root segment
|
||||
return { name: parentPath, parentPath: '' };
|
||||
}
|
||||
47
packages/core/src/lazy.ts
Normal file
47
packages/core/src/lazy.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class Lazy<T> {
|
||||
|
||||
private _didRun: boolean = false;
|
||||
private _value?: T;
|
||||
private _error: Error | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly executor: () => T,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* True if the lazy value has been resolved.
|
||||
*/
|
||||
get hasValue() { return this._didRun; }
|
||||
|
||||
/**
|
||||
* Get the wrapped value.
|
||||
*
|
||||
* This will force evaluation of the lazy value if it has not been resolved yet. Lazy values are only
|
||||
* resolved once. `getValue` will re-throw exceptions that are hit while resolving the value
|
||||
*/
|
||||
get value(): T {
|
||||
if (!this._didRun) {
|
||||
try {
|
||||
this._value = this.executor();
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
} finally {
|
||||
this._didRun = true;
|
||||
}
|
||||
}
|
||||
if (this._error) {
|
||||
throw this._error;
|
||||
}
|
||||
return this._value!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wrapped value without forcing evaluation.
|
||||
*/
|
||||
get rawValue(): T | undefined { return this._value; }
|
||||
}
|
||||
829
packages/core/src/lifecycle.ts
Normal file
829
packages/core/src/lifecycle.ts
Normal file
@ -0,0 +1,829 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { compareBy, numberComparator } from './arrays.js';
|
||||
import { groupBy } from './collections.js';
|
||||
import { SetMap } from './map.js';
|
||||
import { createSingleCallFunction } from './functional.js';
|
||||
import { Iterable } from './iterator.js';
|
||||
|
||||
// #region Disposable Tracking
|
||||
|
||||
/**
|
||||
* Enables logging of potentially leaked disposables.
|
||||
*
|
||||
* A disposable is considered leaked if it is not disposed or not registered as the child of
|
||||
* another disposable. This tracking is very simple an only works for classes that either
|
||||
* extend Disposable or use a DisposableStore. This means there are a lot of false positives.
|
||||
*/
|
||||
const TRACK_DISPOSABLES = false;
|
||||
let disposableTracker: IDisposableTracker | null = null;
|
||||
|
||||
export interface IDisposableTracker {
|
||||
/**
|
||||
* Is called on construction of a disposable.
|
||||
*/
|
||||
trackDisposable(disposable: IDisposable): void;
|
||||
|
||||
/**
|
||||
* Is called when a disposable is registered as child of another disposable (e.g. {@link DisposableStore}).
|
||||
* If parent is `null`, the disposable is removed from its former parent.
|
||||
*/
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void;
|
||||
|
||||
/**
|
||||
* Is called after a disposable is disposed.
|
||||
*/
|
||||
markAsDisposed(disposable: IDisposable): void;
|
||||
|
||||
/**
|
||||
* Indicates that the given object is a singleton which does not need to be disposed.
|
||||
*/
|
||||
markAsSingleton(disposable: IDisposable): void;
|
||||
}
|
||||
|
||||
export class GCBasedDisposableTracker implements IDisposableTracker {
|
||||
|
||||
private readonly _registry = new FinalizationRegistry<string>(heldValue => {
|
||||
console.warn(`[LEAKED DISPOSABLE] ${heldValue}`);
|
||||
});
|
||||
|
||||
trackDisposable(disposable: IDisposable): void {
|
||||
const stack = new Error('CREATED via:').stack!;
|
||||
this._registry.register(disposable, stack, disposable);
|
||||
}
|
||||
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void {
|
||||
if (parent) {
|
||||
this._registry.unregister(child);
|
||||
} else {
|
||||
this.trackDisposable(child);
|
||||
}
|
||||
}
|
||||
|
||||
markAsDisposed(disposable: IDisposable): void {
|
||||
this._registry.unregister(disposable);
|
||||
}
|
||||
|
||||
markAsSingleton(disposable: IDisposable): void {
|
||||
this._registry.unregister(disposable);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DisposableInfo {
|
||||
value: IDisposable;
|
||||
source: string | null;
|
||||
parent: IDisposable | null;
|
||||
isSingleton: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export class DisposableTracker implements IDisposableTracker {
|
||||
private static idx = 0;
|
||||
|
||||
private readonly livingDisposables = new Map<IDisposable, DisposableInfo>();
|
||||
|
||||
private getDisposableData(d: IDisposable): DisposableInfo {
|
||||
let val = this.livingDisposables.get(d);
|
||||
if (!val) {
|
||||
val = { parent: null, source: null, isSingleton: false, value: d, idx: DisposableTracker.idx++ };
|
||||
this.livingDisposables.set(d, val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
trackDisposable(d: IDisposable): void {
|
||||
const data = this.getDisposableData(d);
|
||||
if (!data.source) {
|
||||
data.source =
|
||||
new Error().stack!;
|
||||
}
|
||||
}
|
||||
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void {
|
||||
const data = this.getDisposableData(child);
|
||||
data.parent = parent;
|
||||
}
|
||||
|
||||
markAsDisposed(x: IDisposable): void {
|
||||
this.livingDisposables.delete(x);
|
||||
}
|
||||
|
||||
markAsSingleton(disposable: IDisposable): void {
|
||||
this.getDisposableData(disposable).isSingleton = true;
|
||||
}
|
||||
|
||||
private getRootParent(data: DisposableInfo, cache: Map<DisposableInfo, DisposableInfo>): DisposableInfo {
|
||||
const cacheValue = cache.get(data);
|
||||
if (cacheValue) {
|
||||
return cacheValue;
|
||||
}
|
||||
|
||||
const result = data.parent ? this.getRootParent(this.getDisposableData(data.parent), cache) : data;
|
||||
cache.set(data, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
getTrackedDisposables(): IDisposable[] {
|
||||
const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
|
||||
|
||||
const leaking = [...this.livingDisposables.entries()]
|
||||
.filter(([, v]) => v.source !== null && !this.getRootParent(v, rootParentCache).isSingleton)
|
||||
.flatMap(([k]) => k);
|
||||
|
||||
return leaking;
|
||||
}
|
||||
|
||||
computeLeakingDisposables(maxReported = 10, preComputedLeaks?: DisposableInfo[]): { leaks: DisposableInfo[]; details: string } | undefined {
|
||||
let uncoveredLeakingObjs: DisposableInfo[] | undefined;
|
||||
if (preComputedLeaks) {
|
||||
uncoveredLeakingObjs = preComputedLeaks;
|
||||
} else {
|
||||
const rootParentCache = new Map<DisposableInfo, DisposableInfo>();
|
||||
|
||||
const leakingObjects = [...this.livingDisposables.values()]
|
||||
.filter((info) => info.source !== null && !this.getRootParent(info, rootParentCache).isSingleton);
|
||||
|
||||
if (leakingObjects.length === 0) {
|
||||
return;
|
||||
}
|
||||
const leakingObjsSet = new Set(leakingObjects.map(o => o.value));
|
||||
|
||||
// Remove all objects that are a child of other leaking objects. Assumes there are no cycles.
|
||||
uncoveredLeakingObjs = leakingObjects.filter(l => {
|
||||
return !(l.parent && leakingObjsSet.has(l.parent));
|
||||
});
|
||||
|
||||
if (uncoveredLeakingObjs.length === 0) {
|
||||
throw new Error('There are cyclic diposable chains!');
|
||||
}
|
||||
}
|
||||
|
||||
if (!uncoveredLeakingObjs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getStackTracePath(leaking: DisposableInfo): string[] {
|
||||
function removePrefix(array: string[], linesToRemove: (string | RegExp)[]) {
|
||||
while (array.length > 0 && linesToRemove.some(regexp => typeof regexp === 'string' ? regexp === array[0] : array[0].match(regexp))) {
|
||||
array.shift();
|
||||
}
|
||||
}
|
||||
|
||||
const lines = leaking.source!.split('\n').map(p => p.trim().replace('at ', '')).filter(l => l !== '');
|
||||
removePrefix(lines, ['Error', /^trackDisposable \(.*\)$/, /^DisposableTracker.trackDisposable \(.*\)$/]);
|
||||
return lines.reverse();
|
||||
}
|
||||
|
||||
const stackTraceStarts = new SetMap<string, DisposableInfo>();
|
||||
for (const leaking of uncoveredLeakingObjs) {
|
||||
const stackTracePath = getStackTracePath(leaking);
|
||||
for (let i = 0; i <= stackTracePath.length; i++) {
|
||||
stackTraceStarts.add(stackTracePath.slice(0, i).join('\n'), leaking);
|
||||
}
|
||||
}
|
||||
|
||||
// Put earlier leaks first
|
||||
uncoveredLeakingObjs.sort(compareBy(l => l.idx, numberComparator));
|
||||
|
||||
let message = '';
|
||||
|
||||
let i = 0;
|
||||
for (const leaking of uncoveredLeakingObjs.slice(0, maxReported)) {
|
||||
i++;
|
||||
const stackTracePath = getStackTracePath(leaking);
|
||||
const stackTraceFormattedLines:any = [];
|
||||
|
||||
for (let i = 0; i < stackTracePath.length; i++) {
|
||||
let line = stackTracePath[i];
|
||||
const starts = stackTraceStarts.get(stackTracePath.slice(0, i + 1).join('\n'));
|
||||
line = `(shared with ${starts.size}/${uncoveredLeakingObjs.length} leaks) at ${line}`;
|
||||
|
||||
const prevStarts = stackTraceStarts.get(stackTracePath.slice(0, i).join('\n'));
|
||||
const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v);
|
||||
delete continuations[stackTracePath[i]];
|
||||
for (const [cont, set] of Object.entries(continuations)) {
|
||||
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`);
|
||||
}
|
||||
|
||||
stackTraceFormattedLines.unshift(line);
|
||||
}
|
||||
|
||||
message += `\n\n\n==================== Leaking disposable ${i}/${uncoveredLeakingObjs.length}: ${leaking.value.constructor.name} ====================\n${stackTraceFormattedLines.join('\n')}\n============================================================\n\n`;
|
||||
}
|
||||
|
||||
if (uncoveredLeakingObjs.length > maxReported) {
|
||||
message += `\n\n\n... and ${uncoveredLeakingObjs.length - maxReported} more leaking disposables\n\n`;
|
||||
}
|
||||
|
||||
return { leaks: uncoveredLeakingObjs, details: message };
|
||||
}
|
||||
}
|
||||
|
||||
export function setDisposableTracker(tracker: IDisposableTracker | null): void {
|
||||
disposableTracker = tracker;
|
||||
}
|
||||
|
||||
if (TRACK_DISPOSABLES) {
|
||||
const __is_disposable_tracked__ = '__is_disposable_tracked__';
|
||||
setDisposableTracker(new class implements IDisposableTracker {
|
||||
trackDisposable(x: IDisposable): void {
|
||||
const stack = new Error('Potentially leaked disposable').stack!;
|
||||
setTimeout(() => {
|
||||
if (!(x as any)[__is_disposable_tracked__]) {
|
||||
console.log(stack);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
setParent(child: IDisposable, parent: IDisposable | null): void {
|
||||
if (child && child !== Disposable.None) {
|
||||
try {
|
||||
(child as any)[__is_disposable_tracked__] = true;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markAsDisposed(disposable: IDisposable): void {
|
||||
if (disposable && disposable !== Disposable.None) {
|
||||
try {
|
||||
(disposable as any)[__is_disposable_tracked__] = true;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
markAsSingleton(disposable: IDisposable): void { }
|
||||
});
|
||||
}
|
||||
|
||||
export function trackDisposable<T extends IDisposable>(x: T): T {
|
||||
disposableTracker?.trackDisposable(x);
|
||||
return x;
|
||||
}
|
||||
|
||||
export function markAsDisposed(disposable: IDisposable): void {
|
||||
disposableTracker?.markAsDisposed(disposable);
|
||||
}
|
||||
|
||||
function setParentOfDisposable(child: IDisposable, parent: IDisposable | null): void {
|
||||
disposableTracker?.setParent(child, parent);
|
||||
}
|
||||
|
||||
function setParentOfDisposables(children: IDisposable[], parent: IDisposable | null): void {
|
||||
if (!disposableTracker) {
|
||||
return;
|
||||
}
|
||||
for (const child of children) {
|
||||
disposableTracker.setParent(child, parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the given object is a singleton which does not need to be disposed.
|
||||
*/
|
||||
export function markAsSingleton<T extends IDisposable>(singleton: T): T {
|
||||
disposableTracker?.markAsSingleton(singleton);
|
||||
return singleton;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* An object that performs a cleanup operation when `.dispose()` is called.
|
||||
*
|
||||
* Some examples of how disposables are used:
|
||||
*
|
||||
* - An event listener that removes itself when `.dispose()` is called.
|
||||
* - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called.
|
||||
* - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `thing` is {@link IDisposable disposable}.
|
||||
*/
|
||||
export function isDisposable<E extends any>(thing: E): thing is E & IDisposable {
|
||||
return typeof thing === 'object' && thing !== null && typeof (<IDisposable><any>thing).dispose === 'function' && (<IDisposable><any>thing).dispose.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the value(s) passed in.
|
||||
*/
|
||||
export function dispose<T extends IDisposable>(disposable: T): T;
|
||||
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
|
||||
export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(disposables: A): A;
|
||||
export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>;
|
||||
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
|
||||
export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
|
||||
if (Iterable.is(arg)) {
|
||||
const errors: any[] = [];
|
||||
|
||||
for (const d of arg) {
|
||||
if (d) {
|
||||
try {
|
||||
d.dispose();
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 1) {
|
||||
throw errors[0];
|
||||
} else if (errors.length > 1) {
|
||||
throw new AggregateError(errors, 'Encountered errors while disposing of store');
|
||||
}
|
||||
|
||||
return Array.isArray(arg) ? [] : arg;
|
||||
} else if (arg) {
|
||||
arg.dispose();
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
export function disposeIfDisposable<T extends IDisposable | object>(disposables: Array<T>): Array<T> {
|
||||
for (const d of disposables) {
|
||||
if (isDisposable(d)) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple disposable values into a single {@link IDisposable}.
|
||||
*/
|
||||
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
||||
const parent = toDisposable(() => dispose(disposables));
|
||||
setParentOfDisposables(disposables, parent);
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a function that implements dispose into an {@link IDisposable}.
|
||||
*
|
||||
* @param fn Clean up function, guaranteed to be called only **once**.
|
||||
*/
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
const self = trackDisposable({
|
||||
dispose: createSingleCallFunction(() => {
|
||||
markAsDisposed(self);
|
||||
fn();
|
||||
})
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a collection of disposable values.
|
||||
*
|
||||
* This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an
|
||||
* `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a
|
||||
* store that has already been disposed of.
|
||||
*/
|
||||
export class DisposableStore implements IDisposable {
|
||||
|
||||
static DISABLE_DISPOSED_WARNING = false;
|
||||
|
||||
private readonly _toDispose = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables and mark this object as disposed.
|
||||
*
|
||||
* Any future disposables added to this object will be disposed of on `add`.
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
markAsDisposed(this);
|
||||
this._isDisposed = true;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return `true` if this object has been disposed of.
|
||||
*/
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of all registered disposables but do not mark this object as disposed.
|
||||
*/
|
||||
public clear(): void {
|
||||
if (this._toDispose.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispose(this._toDispose);
|
||||
} finally {
|
||||
this._toDispose.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new {@link IDisposable disposable} to the collection.
|
||||
*/
|
||||
public add<T extends IDisposable>(o: T): T {
|
||||
if (!o) {
|
||||
return o;
|
||||
}
|
||||
if ((o as unknown as DisposableStore) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
|
||||
setParentOfDisposable(o, this);
|
||||
if (this._isDisposed) {
|
||||
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
|
||||
console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
|
||||
}
|
||||
} else {
|
||||
this._toDispose.add(o);
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the
|
||||
* disposable even when the disposable is not part in the store.
|
||||
*/
|
||||
public delete<T extends IDisposable>(o: T): void {
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
if ((o as unknown as DisposableStore) === this) {
|
||||
throw new Error('Cannot dispose a disposable on itself!');
|
||||
}
|
||||
this._toDispose.delete(o);
|
||||
o.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the value from the store, but does not dispose it.
|
||||
*/
|
||||
public deleteAndLeak<T extends IDisposable>(o: T): void {
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
if (this._toDispose.has(o)) {
|
||||
this._toDispose.delete(o);
|
||||
setParentOfDisposable(o, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for a {@link IDisposable disposable} object.
|
||||
*
|
||||
* Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of.
|
||||
*/
|
||||
export abstract class Disposable implements IDisposable {
|
||||
|
||||
/**
|
||||
* A disposable that does nothing when it is disposed of.
|
||||
*
|
||||
* TODO: This should not be a static property.
|
||||
*/
|
||||
static readonly None = Object.freeze<IDisposable>({ dispose() { } });
|
||||
|
||||
protected readonly _store = new DisposableStore();
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
setParentOfDisposable(this._store, this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
markAsDisposed(this);
|
||||
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `o` to the collection of disposables managed by this object.
|
||||
*/
|
||||
protected _register<T extends IDisposable>(o: T): T {
|
||||
if ((o as unknown as Disposable) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
return this._store.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of a disposable value that may be changed.
|
||||
*
|
||||
* This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can
|
||||
* also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up.
|
||||
*/
|
||||
export class MutableDisposable<T extends IDisposable> implements IDisposable {
|
||||
private _value?: T;
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
get value(): T | undefined {
|
||||
return this._isDisposed ? undefined : this._value;
|
||||
}
|
||||
|
||||
set value(value: T | undefined) {
|
||||
if (this._isDisposed || value === this._value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._value?.dispose();
|
||||
if (value) {
|
||||
setParentOfDisposable(value, this);
|
||||
}
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the stored value and disposed of the previously stored value.
|
||||
*/
|
||||
clear(): void {
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._isDisposed = true;
|
||||
markAsDisposed(this);
|
||||
this._value?.dispose();
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the value, but does not dispose it.
|
||||
* The old value is returned.
|
||||
*/
|
||||
clearAndLeak(): T | undefined {
|
||||
const oldValue = this._value;
|
||||
this._value = undefined;
|
||||
if (oldValue) {
|
||||
setParentOfDisposable(oldValue, null);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of a disposable value that may be changed like {@link MutableDisposable}, but the value must
|
||||
* exist and cannot be undefined.
|
||||
*/
|
||||
export class MandatoryMutableDisposable<T extends IDisposable> implements IDisposable {
|
||||
private readonly _disposable = new MutableDisposable<T>();
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor(initialValue: T) {
|
||||
this._disposable.value = initialValue;
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._disposable.value!;
|
||||
}
|
||||
|
||||
set value(value: T) {
|
||||
if (this._isDisposed || value === this._disposable.value) {
|
||||
return;
|
||||
}
|
||||
this._disposable.value = value;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._isDisposed = true;
|
||||
this._disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class RefCountedDisposable {
|
||||
|
||||
private _counter: number = 1;
|
||||
|
||||
constructor(
|
||||
private readonly _disposable: IDisposable,
|
||||
) { }
|
||||
|
||||
acquire() {
|
||||
this._counter++;
|
||||
return this;
|
||||
}
|
||||
|
||||
release() {
|
||||
if (--this._counter === 0) {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A safe disposable can be `unset` so that a leaked reference (listener)
|
||||
* can be cut-off.
|
||||
*/
|
||||
export class SafeDisposable implements IDisposable {
|
||||
|
||||
dispose: () => void = () => { };
|
||||
unset: () => void = () => { };
|
||||
isset: () => boolean = () => false;
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
set(fn: Function) {
|
||||
let callback: Function | undefined = fn;
|
||||
this.unset = () => callback = undefined;
|
||||
this.isset = () => callback !== undefined;
|
||||
this.dispose = () => {
|
||||
if (callback) {
|
||||
callback();
|
||||
callback = undefined;
|
||||
markAsDisposed(this);
|
||||
}
|
||||
};
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IReference<T> extends IDisposable {
|
||||
readonly object: T;
|
||||
}
|
||||
|
||||
export abstract class ReferenceCollection<T> {
|
||||
|
||||
private readonly references: Map<string, { readonly object: T; counter: number }> = new Map();
|
||||
|
||||
acquire(key: string, ...args: any[]): IReference<T> {
|
||||
let reference = this.references.get(key);
|
||||
|
||||
if (!reference) {
|
||||
reference = { counter: 0, object: this.createReferencedObject(key, ...args) };
|
||||
this.references.set(key, reference);
|
||||
}
|
||||
|
||||
const { object } = reference;
|
||||
const dispose = createSingleCallFunction(() => {
|
||||
if (--reference.counter === 0) {
|
||||
this.destroyReferencedObject(key, reference.object);
|
||||
this.references.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
reference.counter++;
|
||||
|
||||
return { object, dispose };
|
||||
}
|
||||
|
||||
protected abstract createReferencedObject(key: string, ...args: any[]): T;
|
||||
protected abstract destroyReferencedObject(key: string, object: T): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps a reference collection of promised values. Makes sure
|
||||
* references are disposed whenever promises get rejected.
|
||||
*/
|
||||
export class AsyncReferenceCollection<T> {
|
||||
|
||||
constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { }
|
||||
|
||||
async acquire(key: string, ...args: any[]): Promise<IReference<T>> {
|
||||
const ref = this.referenceCollection.acquire(key, ...args);
|
||||
|
||||
try {
|
||||
const object = await ref.object;
|
||||
|
||||
return {
|
||||
object,
|
||||
dispose: () => ref.dispose()
|
||||
};
|
||||
} catch (error) {
|
||||
ref.dispose();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ImmortalReference<T> implements IReference<T> {
|
||||
constructor(public object: T) { }
|
||||
dispose(): void { /* noop */ }
|
||||
}
|
||||
|
||||
export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
|
||||
const store = new DisposableStore();
|
||||
try {
|
||||
fn(store);
|
||||
} finally {
|
||||
store.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map the manages the lifecycle of the values that it stores.
|
||||
*/
|
||||
export class DisposableMap<K, V extends IDisposable = IDisposable> implements IDisposable {
|
||||
|
||||
private readonly _store = new Map<K, V>();
|
||||
private _isDisposed = false;
|
||||
|
||||
constructor() {
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of all stored values and mark this object as disposed.
|
||||
*
|
||||
* Trying to use this object after it has been disposed of is an error.
|
||||
*/
|
||||
dispose(): void {
|
||||
markAsDisposed(this);
|
||||
this._isDisposed = true;
|
||||
this.clearAndDisposeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of all stored values and clear the map, but DO NOT mark this object as disposed.
|
||||
*/
|
||||
clearAndDisposeAll(): void {
|
||||
if (!this._store.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispose(this._store.values());
|
||||
} finally {
|
||||
this._store.clear();
|
||||
}
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this._store.has(key);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._store.size;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this._store.get(key);
|
||||
}
|
||||
|
||||
set(key: K, value: V, skipDisposeOnOverwrite = false): void {
|
||||
if (this._isDisposed) {
|
||||
console.warn(new Error('Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!').stack);
|
||||
}
|
||||
|
||||
if (!skipDisposeOnOverwrite) {
|
||||
this._store.get(key)?.dispose();
|
||||
}
|
||||
|
||||
this._store.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the value stored for `key` from this map and also dispose of it.
|
||||
*/
|
||||
deleteAndDispose(key: K): void {
|
||||
this._store.get(key)?.dispose();
|
||||
this._store.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the value stored for `key` from this map but return it. The caller is
|
||||
* responsible for disposing of the value.
|
||||
*/
|
||||
deleteAndLeak(key: K): V | undefined {
|
||||
const value = this._store.get(key);
|
||||
this._store.delete(key);
|
||||
return value;
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
return this._store.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return this._store.values();
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this._store[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
142
packages/core/src/linkedList.ts
Normal file
142
packages/core/src/linkedList.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
class Node<E> {
|
||||
|
||||
static readonly Undefined = new Node<any>(undefined);
|
||||
|
||||
element: E;
|
||||
next: Node<E>;
|
||||
prev: Node<E>;
|
||||
|
||||
constructor(element: E) {
|
||||
this.element = element;
|
||||
this.next = Node.Undefined;
|
||||
this.prev = Node.Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkedList<E> {
|
||||
|
||||
private _first: Node<E> = Node.Undefined;
|
||||
private _last: Node<E> = Node.Undefined;
|
||||
private _size: number = 0;
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._first === Node.Undefined;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
const next = node.next;
|
||||
node.prev = Node.Undefined;
|
||||
node.next = Node.Undefined;
|
||||
node = next;
|
||||
}
|
||||
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
unshift(element: E): () => void {
|
||||
return this._insert(element, false);
|
||||
}
|
||||
|
||||
push(element: E): () => void {
|
||||
return this._insert(element, true);
|
||||
}
|
||||
|
||||
private _insert(element: E, atTheEnd: boolean): () => void {
|
||||
const newNode = new Node(element);
|
||||
if (this._first === Node.Undefined) {
|
||||
this._first = newNode;
|
||||
this._last = newNode;
|
||||
|
||||
} else if (atTheEnd) {
|
||||
// push
|
||||
const oldLast = this._last;
|
||||
this._last = newNode;
|
||||
newNode.prev = oldLast;
|
||||
oldLast.next = newNode;
|
||||
|
||||
} else {
|
||||
// unshift
|
||||
const oldFirst = this._first;
|
||||
this._first = newNode;
|
||||
newNode.next = oldFirst;
|
||||
oldFirst.prev = newNode;
|
||||
}
|
||||
this._size += 1;
|
||||
|
||||
let didRemove = false;
|
||||
return () => {
|
||||
if (!didRemove) {
|
||||
didRemove = true;
|
||||
this._remove(newNode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
shift(): E | undefined {
|
||||
if (this._first === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._first.element;
|
||||
this._remove(this._first);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
pop(): E | undefined {
|
||||
if (this._last === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._last.element;
|
||||
this._remove(this._last);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
private _remove(node: Node<E>): void {
|
||||
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
|
||||
// middle
|
||||
const anchor = node.prev;
|
||||
anchor.next = node.next;
|
||||
node.next.prev = anchor;
|
||||
|
||||
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
|
||||
// only node
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
|
||||
} else if (node.next === Node.Undefined) {
|
||||
// last
|
||||
this._last = this._last.prev!;
|
||||
this._last.next = Node.Undefined;
|
||||
|
||||
} else if (node.prev === Node.Undefined) {
|
||||
// first
|
||||
this._first = this._first.next!;
|
||||
this._first.prev = Node.Undefined;
|
||||
}
|
||||
|
||||
// done
|
||||
this._size -= 1;
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): Iterator<E> {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
yield node.element;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
952
packages/core/src/map.ts
Normal file
952
packages/core/src/map.ts
Normal file
@ -0,0 +1,952 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from './uri.js';
|
||||
|
||||
export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
let result = map.get(key);
|
||||
if (result === undefined) {
|
||||
result = value;
|
||||
map.set(key, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mapToString<K, V>(map: Map<K, V>): string {
|
||||
const entries: string[] = [];
|
||||
map.forEach((value, key) => {
|
||||
entries.push(`${key} => ${value}`);
|
||||
});
|
||||
|
||||
return `Map(${map.size}) {${entries.join(', ')}}`;
|
||||
}
|
||||
|
||||
export function setToString<K>(set: Set<K>): string {
|
||||
const entries: K[] = [];
|
||||
set.forEach(value => {
|
||||
entries.push(value);
|
||||
});
|
||||
|
||||
return `Set(${set.size}) {${entries.join(', ')}}`;
|
||||
}
|
||||
|
||||
interface ResourceMapKeyFn {
|
||||
(resource: URI): string;
|
||||
}
|
||||
|
||||
class ResourceMapEntry<T> {
|
||||
constructor(readonly uri: URI, readonly value: T) { }
|
||||
}
|
||||
|
||||
function isEntries<T>(arg: ResourceMap<T> | ResourceMapKeyFn | readonly (readonly [URI, T])[] | undefined): arg is readonly (readonly [URI, T])[] {
|
||||
return Array.isArray(arg);
|
||||
}
|
||||
|
||||
export class ResourceMap<T> implements Map<URI, T> {
|
||||
|
||||
private static readonly defaultToKey = (resource: URI) => resource.toString();
|
||||
|
||||
readonly [Symbol.toStringTag] = 'ResourceMap';
|
||||
|
||||
private readonly map: Map<string, ResourceMapEntry<T>>;
|
||||
private readonly toKey: ResourceMapKeyFn;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
|
||||
*/
|
||||
constructor(toKey?: ResourceMapKeyFn);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param other Another resource which this maps is created from
|
||||
* @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
|
||||
*/
|
||||
constructor(other?: ResourceMap<T>, toKey?: ResourceMapKeyFn);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param other Another resource which this maps is created from
|
||||
* @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util
|
||||
*/
|
||||
constructor(entries?: readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn);
|
||||
|
||||
constructor(arg?: ResourceMap<T> | ResourceMapKeyFn | readonly (readonly [URI, T])[], toKey?: ResourceMapKeyFn) {
|
||||
if (arg instanceof ResourceMap) {
|
||||
this.map = new Map(arg.map);
|
||||
this.toKey = toKey ?? ResourceMap.defaultToKey;
|
||||
} else if (isEntries(arg)) {
|
||||
this.map = new Map();
|
||||
this.toKey = toKey ?? ResourceMap.defaultToKey;
|
||||
|
||||
for (const [resource, value] of arg) {
|
||||
this.set(resource, value);
|
||||
}
|
||||
} else {
|
||||
this.map = new Map();
|
||||
this.toKey = arg ?? ResourceMap.defaultToKey;
|
||||
}
|
||||
}
|
||||
|
||||
set(resource: URI, value: T): this {
|
||||
this.map.set(this.toKey(resource), new ResourceMapEntry(resource, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
get(resource: URI): T | undefined {
|
||||
return this.map.get(this.toKey(resource))?.value;
|
||||
}
|
||||
|
||||
has(resource: URI): boolean {
|
||||
return this.map.has(this.toKey(resource));
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.map.clear();
|
||||
}
|
||||
|
||||
delete(resource: URI): boolean {
|
||||
return this.map.delete(this.toKey(resource));
|
||||
}
|
||||
|
||||
forEach(clb: (value: T, key: URI, map: Map<URI, T>) => void, thisArg?: any): void {
|
||||
if (typeof thisArg !== 'undefined') {
|
||||
clb = clb.bind(thisArg);
|
||||
}
|
||||
for (const [_, entry] of this.map) {
|
||||
clb(entry.value, entry.uri, <any>this);
|
||||
}
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<URI> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield entry.uri;
|
||||
}
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[URI, T]> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield [entry.uri, entry.value];
|
||||
}
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): IterableIterator<[URI, T]> {
|
||||
for (const [, entry] of this.map) {
|
||||
yield [entry.uri, entry.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceSet implements Set<URI> {
|
||||
|
||||
readonly [Symbol.toStringTag]: string = 'ResourceSet';
|
||||
|
||||
private readonly _map: ResourceMap<URI>;
|
||||
|
||||
constructor(toKey?: ResourceMapKeyFn);
|
||||
constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn);
|
||||
constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) {
|
||||
if (!entriesOrKey || typeof entriesOrKey === 'function') {
|
||||
this._map = new ResourceMap(entriesOrKey as ResourceMapKeyFn);
|
||||
} else {
|
||||
this._map = new ResourceMap(toKey);
|
||||
entriesOrKey.forEach(this.add, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get size(): number {
|
||||
return this._map.size;
|
||||
}
|
||||
|
||||
add(value: URI): this {
|
||||
this._map.set(value, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._map.clear();
|
||||
}
|
||||
|
||||
delete(value: URI): boolean {
|
||||
return this._map.delete(value);
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: URI, value2: URI, set: Set<URI>) => void, thisArg?: any): void {
|
||||
this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this));
|
||||
}
|
||||
|
||||
has(value: URI): boolean {
|
||||
return this._map.has(value);
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[URI, URI]> {
|
||||
return this._map.entries();
|
||||
}
|
||||
|
||||
keys(): IterableIterator<URI> {
|
||||
return this._map.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<URI> {
|
||||
return this._map.keys();
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<URI> {
|
||||
return this.keys();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface Item<K, V> {
|
||||
previous: Item<K, V> | undefined;
|
||||
next: Item<K, V> | undefined;
|
||||
key: K;
|
||||
value: V;
|
||||
}
|
||||
|
||||
export const enum Touch {
|
||||
None = 0,
|
||||
AsOld = 1,
|
||||
AsNew = 2
|
||||
}
|
||||
|
||||
export class LinkedMap<K, V> implements Map<K, V> {
|
||||
|
||||
readonly [Symbol.toStringTag] = 'LinkedMap';
|
||||
|
||||
private _map: Map<K, Item<K, V>>;
|
||||
private _head: Item<K, V> | undefined;
|
||||
private _tail: Item<K, V> | undefined;
|
||||
private _size: number;
|
||||
|
||||
private _state: number;
|
||||
|
||||
constructor() {
|
||||
this._map = new Map<K, Item<K, V>>();
|
||||
this._head = undefined;
|
||||
this._tail = undefined;
|
||||
this._size = 0;
|
||||
this._state = 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._map.clear();
|
||||
this._head = undefined;
|
||||
this._tail = undefined;
|
||||
this._size = 0;
|
||||
this._state++;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return !this._head && !this._tail;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
get first(): V | undefined {
|
||||
return this._head?.value;
|
||||
}
|
||||
|
||||
get last(): V | undefined {
|
||||
return this._tail?.value;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this._map.has(key);
|
||||
}
|
||||
|
||||
get(key: K, touch: Touch = Touch.None): V | undefined {
|
||||
const item = this._map.get(key);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
if (touch !== Touch.None) {
|
||||
this.touch(item, touch);
|
||||
}
|
||||
return item.value;
|
||||
}
|
||||
|
||||
set(key: K, value: V, touch: Touch = Touch.None): this {
|
||||
let item = this._map.get(key);
|
||||
if (item) {
|
||||
item.value = value;
|
||||
if (touch !== Touch.None) {
|
||||
this.touch(item, touch);
|
||||
}
|
||||
} else {
|
||||
item = { key, value, next: undefined, previous: undefined };
|
||||
switch (touch) {
|
||||
case Touch.None:
|
||||
this.addItemLast(item);
|
||||
break;
|
||||
case Touch.AsOld:
|
||||
this.addItemFirst(item);
|
||||
break;
|
||||
case Touch.AsNew:
|
||||
this.addItemLast(item);
|
||||
break;
|
||||
default:
|
||||
this.addItemLast(item);
|
||||
break;
|
||||
}
|
||||
this._map.set(key, item);
|
||||
this._size++;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
return !!this.remove(key);
|
||||
}
|
||||
|
||||
remove(key: K): V | undefined {
|
||||
const item = this._map.get(key);
|
||||
if (!item) {
|
||||
return undefined;
|
||||
}
|
||||
this._map.delete(key);
|
||||
this.removeItem(item);
|
||||
this._size--;
|
||||
return item.value;
|
||||
}
|
||||
|
||||
shift(): V | undefined {
|
||||
if (!this._head && !this._tail) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this._head || !this._tail) {
|
||||
throw new Error('Invalid list');
|
||||
}
|
||||
const item = this._head;
|
||||
this._map.delete(item.key);
|
||||
this.removeItem(item);
|
||||
this._size--;
|
||||
return item.value;
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: LinkedMap<K, V>) => void, thisArg?: any): void {
|
||||
const state = this._state;
|
||||
let current = this._head;
|
||||
while (current) {
|
||||
if (thisArg) {
|
||||
callbackfn.bind(thisArg)(current.value, current.key, this);
|
||||
} else {
|
||||
callbackfn(current.value, current.key, this);
|
||||
}
|
||||
if (this._state !== state) {
|
||||
throw new Error(`LinkedMap got modified during iteration.`);
|
||||
}
|
||||
current = current.next;
|
||||
}
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
const map = this;
|
||||
const state = this._state;
|
||||
let current = this._head;
|
||||
const iterator: IterableIterator<K> = {
|
||||
[Symbol.iterator]() {
|
||||
return iterator;
|
||||
},
|
||||
next(): IteratorResult<K> {
|
||||
if (map._state !== state) {
|
||||
throw new Error(`LinkedMap got modified during iteration.`);
|
||||
}
|
||||
if (current) {
|
||||
const result = { value: current.key, done: false };
|
||||
current = current.next;
|
||||
return result;
|
||||
} else {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
}
|
||||
};
|
||||
return iterator;
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
const map = this;
|
||||
const state = this._state;
|
||||
let current = this._head;
|
||||
const iterator: IterableIterator<V> = {
|
||||
[Symbol.iterator]() {
|
||||
return iterator;
|
||||
},
|
||||
next(): IteratorResult<V> {
|
||||
if (map._state !== state) {
|
||||
throw new Error(`LinkedMap got modified during iteration.`);
|
||||
}
|
||||
if (current) {
|
||||
const result = { value: current.value, done: false };
|
||||
current = current.next;
|
||||
return result;
|
||||
} else {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
}
|
||||
};
|
||||
return iterator;
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[K, V]> {
|
||||
const map = this;
|
||||
const state = this._state;
|
||||
let current = this._head;
|
||||
const iterator: IterableIterator<[K, V]> = {
|
||||
[Symbol.iterator]() {
|
||||
return iterator;
|
||||
},
|
||||
next(): IteratorResult<[K, V]> {
|
||||
if (map._state !== state) {
|
||||
throw new Error(`LinkedMap got modified during iteration.`);
|
||||
}
|
||||
if (current) {
|
||||
const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false };
|
||||
current = current.next;
|
||||
return result;
|
||||
} else {
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
}
|
||||
};
|
||||
return iterator;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
protected trimOld(newSize: number) {
|
||||
if (newSize >= this.size) {
|
||||
return;
|
||||
}
|
||||
if (newSize === 0) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
let current = this._head;
|
||||
let currentSize = this.size;
|
||||
while (current && currentSize > newSize) {
|
||||
this._map.delete(current.key);
|
||||
current = current.next;
|
||||
currentSize--;
|
||||
}
|
||||
this._head = current;
|
||||
this._size = currentSize;
|
||||
if (current) {
|
||||
current.previous = undefined;
|
||||
}
|
||||
this._state++;
|
||||
}
|
||||
|
||||
protected trimNew(newSize: number) {
|
||||
if (newSize >= this.size) {
|
||||
return;
|
||||
}
|
||||
if (newSize === 0) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
let current = this._tail;
|
||||
let currentSize = this.size;
|
||||
while (current && currentSize > newSize) {
|
||||
this._map.delete(current.key);
|
||||
current = current.previous;
|
||||
currentSize--;
|
||||
}
|
||||
this._tail = current;
|
||||
this._size = currentSize;
|
||||
if (current) {
|
||||
current.next = undefined;
|
||||
}
|
||||
this._state++;
|
||||
}
|
||||
|
||||
private addItemFirst(item: Item<K, V>): void {
|
||||
// First time Insert
|
||||
if (!this._head && !this._tail) {
|
||||
this._tail = item;
|
||||
} else if (!this._head) {
|
||||
throw new Error('Invalid list');
|
||||
} else {
|
||||
item.next = this._head;
|
||||
this._head.previous = item;
|
||||
}
|
||||
this._head = item;
|
||||
this._state++;
|
||||
}
|
||||
|
||||
private addItemLast(item: Item<K, V>): void {
|
||||
// First time Insert
|
||||
if (!this._head && !this._tail) {
|
||||
this._head = item;
|
||||
} else if (!this._tail) {
|
||||
throw new Error('Invalid list');
|
||||
} else {
|
||||
item.previous = this._tail;
|
||||
this._tail.next = item;
|
||||
}
|
||||
this._tail = item;
|
||||
this._state++;
|
||||
}
|
||||
|
||||
private removeItem(item: Item<K, V>): void {
|
||||
if (item === this._head && item === this._tail) {
|
||||
this._head = undefined;
|
||||
this._tail = undefined;
|
||||
}
|
||||
else if (item === this._head) {
|
||||
// This can only happen if size === 1 which is handled
|
||||
// by the case above.
|
||||
if (!item.next) {
|
||||
throw new Error('Invalid list');
|
||||
}
|
||||
item.next.previous = undefined;
|
||||
this._head = item.next;
|
||||
}
|
||||
else if (item === this._tail) {
|
||||
// This can only happen if size === 1 which is handled
|
||||
// by the case above.
|
||||
if (!item.previous) {
|
||||
throw new Error('Invalid list');
|
||||
}
|
||||
item.previous.next = undefined;
|
||||
this._tail = item.previous;
|
||||
}
|
||||
else {
|
||||
const next = item.next;
|
||||
const previous = item.previous;
|
||||
if (!next || !previous) {
|
||||
throw new Error('Invalid list');
|
||||
}
|
||||
next.previous = previous;
|
||||
previous.next = next;
|
||||
}
|
||||
item.next = undefined;
|
||||
item.previous = undefined;
|
||||
this._state++;
|
||||
}
|
||||
|
||||
private touch(item: Item<K, V>, touch: Touch): void {
|
||||
if (!this._head || !this._tail) {
|
||||
throw new Error('Invalid list');
|
||||
}
|
||||
if ((touch !== Touch.AsOld && touch !== Touch.AsNew)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (touch === Touch.AsOld) {
|
||||
if (item === this._head) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = item.next;
|
||||
const previous = item.previous;
|
||||
|
||||
// Unlink the item
|
||||
if (item === this._tail) {
|
||||
// previous must be defined since item was not head but is tail
|
||||
// So there are more than on item in the map
|
||||
previous!.next = undefined;
|
||||
this._tail = previous;
|
||||
}
|
||||
else {
|
||||
// Both next and previous are not undefined since item was neither head nor tail.
|
||||
next!.previous = previous;
|
||||
previous!.next = next;
|
||||
}
|
||||
|
||||
// Insert the node at head
|
||||
item.previous = undefined;
|
||||
item.next = this._head;
|
||||
this._head.previous = item;
|
||||
this._head = item;
|
||||
this._state++;
|
||||
} else if (touch === Touch.AsNew) {
|
||||
if (item === this._tail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = item.next;
|
||||
const previous = item.previous;
|
||||
|
||||
// Unlink the item.
|
||||
if (item === this._head) {
|
||||
// next must be defined since item was not tail but is head
|
||||
// So there are more than on item in the map
|
||||
next!.previous = undefined;
|
||||
this._head = next;
|
||||
} else {
|
||||
// Both next and previous are not undefined since item was neither head nor tail.
|
||||
next!.previous = previous;
|
||||
previous!.next = next;
|
||||
}
|
||||
item.next = undefined;
|
||||
item.previous = this._tail;
|
||||
this._tail.next = item;
|
||||
this._tail = item;
|
||||
this._state++;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): [K, V][] {
|
||||
const data: [K, V][] = [];
|
||||
|
||||
this.forEach((value, key) => {
|
||||
data.push([key, value]);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
fromJSON(data: [K, V][]): void {
|
||||
this.clear();
|
||||
|
||||
for (const [key, value] of data) {
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Cache<K, V> extends LinkedMap<K, V> {
|
||||
|
||||
protected _limit: number;
|
||||
protected _ratio: number;
|
||||
|
||||
constructor(limit: number, ratio: number = 1) {
|
||||
super();
|
||||
this._limit = limit;
|
||||
this._ratio = Math.min(Math.max(0, ratio), 1);
|
||||
}
|
||||
|
||||
get limit(): number {
|
||||
return this._limit;
|
||||
}
|
||||
|
||||
set limit(limit: number) {
|
||||
this._limit = limit;
|
||||
this.checkTrim();
|
||||
}
|
||||
|
||||
get ratio(): number {
|
||||
return this._ratio;
|
||||
}
|
||||
|
||||
set ratio(ratio: number) {
|
||||
this._ratio = Math.min(Math.max(0, ratio), 1);
|
||||
this.checkTrim();
|
||||
}
|
||||
|
||||
override get(key: K, touch: Touch = Touch.AsNew): V | undefined {
|
||||
return super.get(key, touch);
|
||||
}
|
||||
|
||||
peek(key: K): V | undefined {
|
||||
return super.get(key, Touch.None);
|
||||
}
|
||||
|
||||
override set(key: K, value: V): this {
|
||||
super.set(key, value, Touch.AsNew);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected checkTrim() {
|
||||
if (this.size > this._limit) {
|
||||
this.trim(Math.round(this._limit * this._ratio));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract trim(newSize: number): void;
|
||||
}
|
||||
|
||||
export class LRUCache<K, V> extends Cache<K, V> {
|
||||
|
||||
constructor(limit: number, ratio: number = 1) {
|
||||
super(limit, ratio);
|
||||
}
|
||||
|
||||
protected override trim(newSize: number) {
|
||||
this.trimOld(newSize);
|
||||
}
|
||||
|
||||
override set(key: K, value: V): this {
|
||||
super.set(key, value);
|
||||
this.checkTrim();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class MRUCache<K, V> extends Cache<K, V> {
|
||||
|
||||
constructor(limit: number, ratio: number = 1) {
|
||||
super(limit, ratio);
|
||||
}
|
||||
|
||||
protected override trim(newSize: number) {
|
||||
this.trimNew(newSize);
|
||||
}
|
||||
|
||||
override set(key: K, value: V): this {
|
||||
if (this._limit <= this.size && !this.has(key)) {
|
||||
this.trim(Math.round(this._limit * this._ratio) - 1);
|
||||
}
|
||||
|
||||
super.set(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class CounterSet<T> {
|
||||
|
||||
private map = new Map<T, number>();
|
||||
|
||||
add(value: T): CounterSet<T> {
|
||||
this.map.set(value, (this.map.get(value) || 0) + 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
let counter = this.map.get(value) || 0;
|
||||
|
||||
if (counter === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
counter--;
|
||||
|
||||
if (counter === 0) {
|
||||
this.map.delete(value);
|
||||
} else {
|
||||
this.map.set(value, counter);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.map.has(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map that allows access both by keys and values.
|
||||
* **NOTE**: values need to be unique.
|
||||
*/
|
||||
export class BidirectionalMap<K, V> {
|
||||
|
||||
private readonly _m1 = new Map<K, V>();
|
||||
private readonly _m2 = new Map<V, K>();
|
||||
|
||||
constructor(entries?: readonly (readonly [K, V])[]) {
|
||||
if (entries) {
|
||||
for (const [key, value] of entries) {
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._m1.clear();
|
||||
this._m2.clear();
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
this._m1.set(key, value);
|
||||
this._m2.set(value, key);
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this._m1.get(key);
|
||||
}
|
||||
|
||||
getKey(value: V): K | undefined {
|
||||
return this._m2.get(value);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const value = this._m1.get(key);
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
this._m1.delete(key);
|
||||
this._m2.delete(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: any): void {
|
||||
this._m1.forEach((value, key) => {
|
||||
callbackfn.call(thisArg, value, key, this);
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
return this._m1.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return this._m1.values();
|
||||
}
|
||||
}
|
||||
|
||||
export class SetMap<K, V> {
|
||||
|
||||
private map = new Map<K, Set<V>>();
|
||||
|
||||
add(key: K, value: V): void {
|
||||
let values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
values = new Set<V>();
|
||||
this.map.set(key, values);
|
||||
}
|
||||
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
delete(key: K, value: V): void {
|
||||
const values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
values.delete(value);
|
||||
|
||||
if (values.size === 0) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
forEach(key: K, fn: (value: V) => void): void {
|
||||
const values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
values.forEach(fn);
|
||||
}
|
||||
|
||||
get(key: K): ReadonlySet<V> {
|
||||
const values = this.map.get(key);
|
||||
if (!values) {
|
||||
return new Set<V>();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key) || b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key] of b) {
|
||||
if (!a.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map that is addressable with an arbitrary number of keys. This is useful in high performance
|
||||
* scenarios where creating a composite key whenever the data is accessed is too expensive. For
|
||||
* example for a very hot function, constructing a string like `first-second-third` for every call
|
||||
* will cause a significant hit to performance.
|
||||
*/
|
||||
export class NKeyMap<TValue, TKeys extends (string | boolean | number)[]> {
|
||||
private _data: Map<any, any> = new Map();
|
||||
|
||||
/**
|
||||
* Sets a value on the map. Note that unlike a standard `Map`, the first argument is the value.
|
||||
* This is because the spread operator is used for the keys and must be last..
|
||||
* @param value The value to set.
|
||||
* @param keys The keys for the value.
|
||||
*/
|
||||
public set(value: TValue, ...keys: [...TKeys]): void {
|
||||
let currentMap = this._data;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!currentMap.has(keys[i])) {
|
||||
currentMap.set(keys[i], new Map());
|
||||
}
|
||||
currentMap = currentMap.get(keys[i]);
|
||||
}
|
||||
currentMap.set(keys[keys.length - 1], value);
|
||||
}
|
||||
|
||||
public get(...keys: [...TKeys]): TValue | undefined {
|
||||
let currentMap = this._data;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!currentMap.has(keys[i])) {
|
||||
return undefined;
|
||||
}
|
||||
currentMap = currentMap.get(keys[i]);
|
||||
}
|
||||
return currentMap.get(keys[keys.length - 1]);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data.clear();
|
||||
}
|
||||
|
||||
public *values(): IterableIterator<TValue> {
|
||||
function* iterate(map: Map<any, any>): IterableIterator<TValue> {
|
||||
for (const value of map.values()) {
|
||||
if (value instanceof Map) {
|
||||
yield* iterate(value);
|
||||
} else {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* iterate(this._data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a textual representation of the map for debugging purposes.
|
||||
*/
|
||||
public toString(): string {
|
||||
const printMap = (map: Map<any, any>, depth: number): string => {
|
||||
let result = '';
|
||||
for (const [key, value] of map) {
|
||||
result += `${' '.repeat(depth)}${key}: `;
|
||||
if (value instanceof Map) {
|
||||
result += '\n' + printMap(value, depth + 1);
|
||||
} else {
|
||||
result += `${value}\n`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return printMap(this._data, 0);
|
||||
}
|
||||
}
|
||||
29
packages/core/src/marshallingIds.ts
Normal file
29
packages/core/src/marshallingIds.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const enum MarshalledId {
|
||||
Uri = 1,
|
||||
Regexp,
|
||||
ScmResource,
|
||||
ScmResourceGroup,
|
||||
ScmProvider,
|
||||
CommentController,
|
||||
CommentThread,
|
||||
CommentThreadInstance,
|
||||
CommentThreadReply,
|
||||
CommentNode,
|
||||
CommentThreadNode,
|
||||
TimelineActionContext,
|
||||
NotebookCellActionContext,
|
||||
NotebookActionContext,
|
||||
TerminalContext,
|
||||
TestItemContext,
|
||||
Date,
|
||||
TestMessageMenuArgs,
|
||||
ChatViewContext,
|
||||
LanguageModelToolResult,
|
||||
LanguageModelTextPart,
|
||||
LanguageModelPromptTsxPart,
|
||||
}
|
||||
126
packages/core/src/mime.ts
Normal file
126
packages/core/src/mime.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extname } from './path.js';
|
||||
|
||||
export const Mimes = Object.freeze({
|
||||
text: 'text/plain',
|
||||
binary: 'application/octet-stream',
|
||||
unknown: 'application/unknown',
|
||||
markdown: 'text/markdown',
|
||||
latex: 'text/latex',
|
||||
uriList: 'text/uri-list',
|
||||
});
|
||||
|
||||
interface MapExtToMediaMimes {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
const mapExtToTextMimes: MapExtToMediaMimes = {
|
||||
'.css': 'text/css',
|
||||
'.csv': 'text/csv',
|
||||
'.htm': 'text/html',
|
||||
'.html': 'text/html',
|
||||
'.ics': 'text/calendar',
|
||||
'.js': 'text/javascript',
|
||||
'.mjs': 'text/javascript',
|
||||
'.txt': 'text/plain',
|
||||
'.xml': 'text/xml'
|
||||
};
|
||||
|
||||
// Known media mimes that we can handle
|
||||
const mapExtToMediaMimes: MapExtToMediaMimes = {
|
||||
'.aac': 'audio/x-aac',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.bmp': 'image/bmp',
|
||||
'.flv': 'video/x-flv',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.jpe': 'image/jpg',
|
||||
'.jpeg': 'image/jpg',
|
||||
'.jpg': 'image/jpg',
|
||||
'.m1v': 'video/mpeg',
|
||||
'.m2a': 'audio/mpeg',
|
||||
'.m2v': 'video/mpeg',
|
||||
'.m3a': 'audio/mpeg',
|
||||
'.mid': 'audio/midi',
|
||||
'.midi': 'audio/midi',
|
||||
'.mk3d': 'video/x-matroska',
|
||||
'.mks': 'video/x-matroska',
|
||||
'.mkv': 'video/x-matroska',
|
||||
'.mov': 'video/quicktime',
|
||||
'.movie': 'video/x-sgi-movie',
|
||||
'.mp2': 'audio/mpeg',
|
||||
'.mp2a': 'audio/mpeg',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.mp4a': 'audio/mp4',
|
||||
'.mp4v': 'video/mp4',
|
||||
'.mpe': 'video/mpeg',
|
||||
'.mpeg': 'video/mpeg',
|
||||
'.mpg': 'video/mpeg',
|
||||
'.mpg4': 'video/mp4',
|
||||
'.mpga': 'audio/mpeg',
|
||||
'.oga': 'audio/ogg',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.opus': 'audio/opus',
|
||||
'.ogv': 'video/ogg',
|
||||
'.png': 'image/png',
|
||||
'.psd': 'image/vnd.adobe.photoshop',
|
||||
'.qt': 'video/quicktime',
|
||||
'.spx': 'audio/ogg',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.tga': 'image/x-tga',
|
||||
'.tif': 'image/tiff',
|
||||
'.tiff': 'image/tiff',
|
||||
'.wav': 'audio/x-wav',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.wma': 'audio/x-ms-wma',
|
||||
'.wmv': 'video/x-ms-wmv',
|
||||
'.woff': 'application/font-woff',
|
||||
};
|
||||
|
||||
export function getMediaOrTextMime(path: string): string | undefined {
|
||||
const ext = extname(path);
|
||||
const textMime = mapExtToTextMimes[ext.toLowerCase()];
|
||||
if (textMime !== undefined) {
|
||||
return textMime;
|
||||
} else {
|
||||
return getMediaMime(path);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMediaMime(path: string): string | undefined {
|
||||
const ext = extname(path);
|
||||
return mapExtToMediaMimes[ext.toLowerCase()];
|
||||
}
|
||||
|
||||
export function getExtensionForMimeType(mimeType: string): string | undefined {
|
||||
for (const extension in mapExtToMediaMimes) {
|
||||
if (mapExtToMediaMimes[extension] === mimeType) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const _simplePattern = /^(.+)\/(.+?)(;.+)?$/;
|
||||
|
||||
export function normalizeMimeType(mimeType: string): string;
|
||||
export function normalizeMimeType(mimeType: string, strict: true): string | undefined;
|
||||
export function normalizeMimeType(mimeType: string, strict?: true): string | undefined {
|
||||
|
||||
const match = _simplePattern.exec(mimeType);
|
||||
if (!match) {
|
||||
return strict
|
||||
? undefined
|
||||
: mimeType;
|
||||
}
|
||||
// https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
|
||||
// media and subtype must ALWAYS be lowercase, parameter not
|
||||
return `${match[1].toLowerCase()}/${match[2].toLowerCase()}${match[3] ?? ''}`;
|
||||
}
|
||||
408
packages/core/src/network.ts
Normal file
408
packages/core/src/network.ts
Normal file
@ -0,0 +1,408 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as errors from './errors.js';
|
||||
import * as platform from './platform.js';
|
||||
import { equalsIgnoreCase, startsWithIgnoreCase } from './strings.js';
|
||||
import { URI } from './uri.js';
|
||||
import * as paths from './path.js';
|
||||
|
||||
export namespace Schemas {
|
||||
|
||||
/**
|
||||
* A schema that is used for models that exist in memory
|
||||
* only and that have no correspondence on a server or such.
|
||||
*/
|
||||
export const inMemory = 'inmemory';
|
||||
|
||||
/**
|
||||
* A schema that is used for setting files
|
||||
*/
|
||||
export const vscode = 'vscode';
|
||||
|
||||
/**
|
||||
* A schema that is used for internal private files
|
||||
*/
|
||||
export const internal = 'private';
|
||||
|
||||
/**
|
||||
* A walk-through document.
|
||||
*/
|
||||
export const walkThrough = 'walkThrough';
|
||||
|
||||
/**
|
||||
* An embedded code snippet.
|
||||
*/
|
||||
export const walkThroughSnippet = 'walkThroughSnippet';
|
||||
|
||||
export const http = 'http';
|
||||
|
||||
export const https = 'https';
|
||||
|
||||
export const file = 'file';
|
||||
|
||||
export const mailto = 'mailto';
|
||||
|
||||
export const untitled = 'untitled';
|
||||
|
||||
export const data = 'data';
|
||||
|
||||
export const command = 'command';
|
||||
|
||||
export const vscodeRemote = 'vscode-remote';
|
||||
|
||||
export const vscodeRemoteResource = 'vscode-remote-resource';
|
||||
|
||||
export const vscodeManagedRemoteResource = 'vscode-managed-remote-resource';
|
||||
|
||||
export const vscodeUserData = 'vscode-userdata';
|
||||
|
||||
export const vscodeCustomEditor = 'vscode-custom-editor';
|
||||
|
||||
export const vscodeNotebookCell = 'vscode-notebook-cell';
|
||||
export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata';
|
||||
export const vscodeNotebookCellMetadataDiff = 'vscode-notebook-cell-metadata-diff';
|
||||
export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output';
|
||||
export const vscodeNotebookCellOutputDiff = 'vscode-notebook-cell-output-diff';
|
||||
export const vscodeNotebookMetadata = 'vscode-notebook-metadata';
|
||||
export const vscodeInteractiveInput = 'vscode-interactive-input';
|
||||
|
||||
export const vscodeSettings = 'vscode-settings';
|
||||
|
||||
export const vscodeWorkspaceTrust = 'vscode-workspace-trust';
|
||||
|
||||
export const vscodeTerminal = 'vscode-terminal';
|
||||
|
||||
/** Scheme used for code blocks in chat. */
|
||||
export const vscodeChatCodeBlock = 'vscode-chat-code-block';
|
||||
|
||||
/** Scheme used for LHS of code compare (aka diff) blocks in chat. */
|
||||
export const vscodeChatCodeCompareBlock = 'vscode-chat-code-compare-block';
|
||||
|
||||
/** Scheme used for the chat input editor. */
|
||||
export const vscodeChatSesssion = 'vscode-chat-editor';
|
||||
|
||||
/**
|
||||
* Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors)
|
||||
*/
|
||||
export const webviewPanel = 'webview-panel';
|
||||
|
||||
/**
|
||||
* Scheme used for loading the wrapper html and script in webviews.
|
||||
*/
|
||||
export const vscodeWebview = 'vscode-webview';
|
||||
|
||||
/**
|
||||
* Scheme used for extension pages
|
||||
*/
|
||||
export const extension = 'extension';
|
||||
|
||||
/**
|
||||
* Scheme used as a replacement of `file` scheme to load
|
||||
* files with our custom protocol handler (desktop only).
|
||||
*/
|
||||
export const vscodeFileResource = 'vscode-file';
|
||||
|
||||
/**
|
||||
* Scheme used for temporary resources
|
||||
*/
|
||||
export const tmp = 'tmp';
|
||||
|
||||
/**
|
||||
* Scheme used vs live share
|
||||
*/
|
||||
export const vsls = 'vsls';
|
||||
|
||||
/**
|
||||
* Scheme used for the Source Control commit input's text document
|
||||
*/
|
||||
export const vscodeSourceControl = 'vscode-scm';
|
||||
|
||||
/**
|
||||
* Scheme used for input box for creating comments.
|
||||
*/
|
||||
export const commentsInput = 'comment';
|
||||
|
||||
/**
|
||||
* Scheme used for special rendering of settings in the release notes
|
||||
*/
|
||||
export const codeSetting = 'code-setting';
|
||||
|
||||
/**
|
||||
* Scheme used for output panel resources
|
||||
*/
|
||||
export const outputChannel = 'output';
|
||||
|
||||
/**
|
||||
* Scheme used for the accessible view
|
||||
*/
|
||||
export const accessibleView = 'accessible-view';
|
||||
}
|
||||
|
||||
export function matchesScheme(target: URI | string, scheme: string): boolean {
|
||||
if (URI.isUri(target)) {
|
||||
return equalsIgnoreCase(target.scheme, scheme);
|
||||
} else {
|
||||
return startsWithIgnoreCase(target, scheme + ':');
|
||||
}
|
||||
}
|
||||
|
||||
export function matchesSomeScheme(target: URI | string, ...schemes: string[]): boolean {
|
||||
return schemes.some(scheme => matchesScheme(target, scheme));
|
||||
}
|
||||
|
||||
export const connectionTokenCookieName = 'vscode-tkn';
|
||||
export const connectionTokenQueryName = 'tkn';
|
||||
|
||||
class RemoteAuthoritiesImpl {
|
||||
private readonly _hosts: { [authority: string]: string | undefined } = Object.create(null);
|
||||
private readonly _ports: { [authority: string]: number | undefined } = Object.create(null);
|
||||
private readonly _connectionTokens: { [authority: string]: string | undefined } = Object.create(null);
|
||||
private _preferredWebSchema: 'http' | 'https' = 'http';
|
||||
private _delegate: ((uri: URI) => URI) | null = null;
|
||||
private _serverRootPath: string = '/';
|
||||
|
||||
setPreferredWebSchema(schema: 'http' | 'https') {
|
||||
this._preferredWebSchema = schema;
|
||||
}
|
||||
|
||||
setDelegate(delegate: (uri: URI) => URI): void {
|
||||
this._delegate = delegate;
|
||||
}
|
||||
|
||||
setServerRootPath(product: { quality?: string; commit?: string }, serverBasePath: string | undefined): void {
|
||||
this._serverRootPath = paths.posix.join(serverBasePath ?? '/', getServerProductSegment(product));
|
||||
}
|
||||
|
||||
getServerRootPath(): string {
|
||||
return this._serverRootPath;
|
||||
}
|
||||
|
||||
private get _remoteResourcesPath(): string {
|
||||
return paths.posix.join(this._serverRootPath, Schemas.vscodeRemoteResource);
|
||||
}
|
||||
|
||||
set(authority: string, host: string, port: number): void {
|
||||
this._hosts[authority] = host;
|
||||
this._ports[authority] = port;
|
||||
}
|
||||
|
||||
setConnectionToken(authority: string, connectionToken: string): void {
|
||||
this._connectionTokens[authority] = connectionToken;
|
||||
}
|
||||
|
||||
getPreferredWebSchema(): 'http' | 'https' {
|
||||
return this._preferredWebSchema;
|
||||
}
|
||||
|
||||
rewrite(uri: URI): URI {
|
||||
if (this._delegate) {
|
||||
try {
|
||||
return this._delegate(uri);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
const authority = uri.authority;
|
||||
let host = this._hosts[authority];
|
||||
if (host && host.indexOf(':') !== -1 && host.indexOf('[') === -1) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
const port = this._ports[authority];
|
||||
const connectionToken = this._connectionTokens[authority];
|
||||
let query = `path=${encodeURIComponent(uri.path)}`;
|
||||
if (typeof connectionToken === 'string') {
|
||||
query += `&${connectionTokenQueryName}=${encodeURIComponent(connectionToken)}`;
|
||||
}
|
||||
return URI.from({
|
||||
scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource,
|
||||
authority: `${host}:${port}`,
|
||||
path: this._remoteResourcesPath,
|
||||
query
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const RemoteAuthorities = new RemoteAuthoritiesImpl();
|
||||
|
||||
export function getServerProductSegment(product: { quality?: string; commit?: string }) {
|
||||
return `${product.quality ?? 'oss'}-${product.commit ?? 'dev'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string pointing to a path inside the app. It should not begin with ./ or ../
|
||||
*/
|
||||
export type AppResourcePath = (
|
||||
`a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}`
|
||||
| `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}`
|
||||
| `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}`
|
||||
| `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}`
|
||||
| `y${string}` | `z${string}`
|
||||
);
|
||||
|
||||
export const builtinExtensionsPath: AppResourcePath = 'vs/../../extensions';
|
||||
export const nodeModulesPath: AppResourcePath = 'vs/../../node_modules';
|
||||
export const nodeModulesAsarPath: AppResourcePath = 'vs/../../node_modules.asar';
|
||||
export const nodeModulesAsarUnpackedPath: AppResourcePath = 'vs/../../node_modules.asar.unpacked';
|
||||
|
||||
export const VSCODE_AUTHORITY = 'vscode-app';
|
||||
|
||||
class FileAccessImpl {
|
||||
|
||||
private static readonly FALLBACK_AUTHORITY = VSCODE_AUTHORITY;
|
||||
|
||||
/**
|
||||
* Returns a URI to use in contexts where the browser is responsible
|
||||
* for loading (e.g. fetch()) or when used within the DOM.
|
||||
*
|
||||
* **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
|
||||
*/
|
||||
asBrowserUri(resourcePath: AppResourcePath | ''): URI {
|
||||
const uri = this.toUri(resourcePath);
|
||||
return this.uriToBrowserUri(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URI to use in contexts where the browser is responsible
|
||||
* for loading (e.g. fetch()) or when used within the DOM.
|
||||
*
|
||||
* **Note:** use `dom.ts#asCSSUrl` whenever the URL is to be used in CSS context.
|
||||
*/
|
||||
uriToBrowserUri(uri: URI): URI {
|
||||
// Handle remote URIs via `RemoteAuthorities`
|
||||
if (uri.scheme === Schemas.vscodeRemote) {
|
||||
return RemoteAuthorities.rewrite(uri);
|
||||
}
|
||||
|
||||
// Convert to `vscode-file` resource..
|
||||
if (
|
||||
// ...only ever for `file` resources
|
||||
uri.scheme === Schemas.file &&
|
||||
(
|
||||
// ...and we run in native environments
|
||||
platform.isNative ||
|
||||
// ...or web worker extensions on desktop
|
||||
(platform.webWorkerOrigin === `${Schemas.vscodeFileResource}://${FileAccessImpl.FALLBACK_AUTHORITY}`)
|
||||
)
|
||||
) {
|
||||
return uri.with({
|
||||
scheme: Schemas.vscodeFileResource,
|
||||
// We need to provide an authority here so that it can serve
|
||||
// as origin for network and loading matters in chromium.
|
||||
// If the URI is not coming with an authority already, we
|
||||
// add our own
|
||||
authority: uri.authority || FileAccessImpl.FALLBACK_AUTHORITY,
|
||||
query: null,
|
||||
fragment: null
|
||||
});
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `file` URI to use in contexts where node.js
|
||||
* is responsible for loading.
|
||||
*/
|
||||
asFileUri(resourcePath: AppResourcePath | ''): URI {
|
||||
const uri = this.toUri(resourcePath);
|
||||
return this.uriToFileUri(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `file` URI to use in contexts where node.js
|
||||
* is responsible for loading.
|
||||
*/
|
||||
uriToFileUri(uri: URI): URI {
|
||||
// Only convert the URI if it is `vscode-file:` scheme
|
||||
if (uri.scheme === Schemas.vscodeFileResource) {
|
||||
return uri.with({
|
||||
scheme: Schemas.file,
|
||||
// Only preserve the `authority` if it is different from
|
||||
// our fallback authority. This ensures we properly preserve
|
||||
// Windows UNC paths that come with their own authority.
|
||||
authority: uri.authority !== FileAccessImpl.FALLBACK_AUTHORITY ? uri.authority : null,
|
||||
query: null,
|
||||
fragment: null
|
||||
});
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
private toUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
|
||||
if (URI.isUri(uriOrModule)) {
|
||||
return uriOrModule;
|
||||
}
|
||||
|
||||
if (globalThis._VSCODE_FILE_ROOT) {
|
||||
const rootUriOrPath = globalThis._VSCODE_FILE_ROOT;
|
||||
|
||||
// File URL (with scheme)
|
||||
if (/^\w[\w\d+.-]*:\/\//.test(rootUriOrPath)) {
|
||||
return URI.joinPath(URI.parse(rootUriOrPath, true), uriOrModule);
|
||||
}
|
||||
|
||||
// File Path (no scheme)
|
||||
const modulePath = paths.join(rootUriOrPath, uriOrModule);
|
||||
return URI.file(modulePath);
|
||||
}
|
||||
|
||||
return URI.parse(moduleIdToUrl!.toUrl(uriOrModule));
|
||||
}
|
||||
}
|
||||
|
||||
export const FileAccess = new FileAccessImpl();
|
||||
|
||||
|
||||
export namespace COI {
|
||||
|
||||
const coiHeaders = new Map<'3' | '2' | '1' | string, Record<string, string>>([
|
||||
['1', { 'Cross-Origin-Opener-Policy': 'same-origin' }],
|
||||
['2', { 'Cross-Origin-Embedder-Policy': 'require-corp' }],
|
||||
['3', { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp' }],
|
||||
]);
|
||||
|
||||
export const CoopAndCoep = Object.freeze(coiHeaders.get('3'));
|
||||
|
||||
const coiSearchParamName = 'vscode-coi';
|
||||
|
||||
/**
|
||||
* Extract desired headers from `vscode-coi` invocation
|
||||
*/
|
||||
export function getHeadersFromQuery(url: string | URI | URL): Record<string, string> | undefined {
|
||||
let params: URLSearchParams | undefined;
|
||||
if (typeof url === 'string') {
|
||||
params = new URL(url).searchParams;
|
||||
} else if (url instanceof URL) {
|
||||
params = url.searchParams;
|
||||
} else if (URI.isUri(url)) {
|
||||
params = new URL(url.toString(true)).searchParams;
|
||||
}
|
||||
const value = params?.get(coiSearchParamName);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return coiHeaders.get(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the `vscode-coi` query attribute based on wanting `COOP` and `COEP`. Will be a noop when `crossOriginIsolated`
|
||||
* isn't enabled the current context
|
||||
*/
|
||||
export function addSearchParam(urlOrSearch: URLSearchParams | Record<string, string>, coop: boolean, coep: boolean): void {
|
||||
if (!(<any>globalThis).crossOriginIsolated) {
|
||||
// depends on the current context being COI
|
||||
return;
|
||||
}
|
||||
const value = coop && coep ? '3' : coep ? '2' : '1';
|
||||
if (urlOrSearch instanceof URLSearchParams) {
|
||||
urlOrSearch.set(coiSearchParamName, value);
|
||||
} else {
|
||||
(<Record<string, string>>urlOrSearch)[coiSearchParamName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
packages/core/src/nls.messages.ts
Normal file
19
packages/core/src/nls.messages.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* This module exists so that the AMD build of the monaco editor can replace this with an async loader plugin.
|
||||
* If you add new functions to this module make sure that they are also provided in the AMD build of the monaco editor.
|
||||
*
|
||||
* TODO@esm remove me once we no longer ship an AMD build.
|
||||
*/
|
||||
|
||||
export function getNLSMessages(): string[] {
|
||||
return globalThis._VSCODE_NLS_MESSAGES;
|
||||
}
|
||||
|
||||
export function getNLSLanguage(): string | undefined {
|
||||
return globalThis._VSCODE_NLS_LANGUAGE;
|
||||
}
|
||||
240
packages/core/src/nls.ts
Normal file
240
packages/core/src/nls.ts
Normal file
@ -0,0 +1,240 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { getNLSLanguage, getNLSMessages } from './nls.messages.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
export { getNLSLanguage, getNLSMessages } from './nls.messages.js';
|
||||
|
||||
const isPseudo = getNLSLanguage() === 'pseudo' || (typeof document !== 'undefined' && document.location && typeof document.location.hash === 'string' && document.location.hash.indexOf('pseudo=true') >= 0);
|
||||
|
||||
export interface ILocalizeInfo {
|
||||
key: string;
|
||||
comment: string[];
|
||||
}
|
||||
|
||||
export interface ILocalizedString {
|
||||
original: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function _format(message: string, args: (string | number | boolean | undefined | null)[]): string {
|
||||
let result: string;
|
||||
|
||||
if (args.length === 0) {
|
||||
result = message;
|
||||
} else {
|
||||
result = message.replace(/\{(\d+)\}/g, (match, rest) => {
|
||||
const index = rest[0];
|
||||
const arg = args[index];
|
||||
let result = match;
|
||||
if (typeof arg === 'string') {
|
||||
result = arg;
|
||||
} else if (typeof arg === 'number' || typeof arg === 'boolean' || arg === void 0 || arg === null) {
|
||||
result = String(arg);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
if (isPseudo) {
|
||||
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
|
||||
result = '\uFF3B' + result.replace(/[aouei]/g, '$&$&') + '\uFF3D';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a string to be localized. Returns the localized string.
|
||||
*
|
||||
* @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
|
||||
* @param message The string to localize
|
||||
* @param args The arguments to the string
|
||||
*
|
||||
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
|
||||
* @example `localize({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
|
||||
*
|
||||
* @returns string The localized string.
|
||||
*/
|
||||
export function localize(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
|
||||
|
||||
/**
|
||||
* Marks a string to be localized. Returns the localized string.
|
||||
*
|
||||
* @param key The key to use for localizing the string
|
||||
* @param message The string to localize
|
||||
* @param args The arguments to the string
|
||||
*
|
||||
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
|
||||
* @example For example, `localize('sayHello', 'hello {0}', name)`
|
||||
*
|
||||
* @returns string The localized string.
|
||||
*/
|
||||
export function localize(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): string;
|
||||
|
||||
/**
|
||||
* @skipMangle
|
||||
*/
|
||||
export function localize(data: ILocalizeInfo | string /* | number when built */, message: string /* | null when built */, ...args: (string | number | boolean | undefined | null)[]): string {
|
||||
if (typeof data === 'number') {
|
||||
return _format(lookupMessage(data, message), args);
|
||||
}
|
||||
return _format(message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used when built: Looks up the message in the global NLS table.
|
||||
* This table is being made available as a global through bootstrapping
|
||||
* depending on the target context.
|
||||
*/
|
||||
function lookupMessage(index: number, fallback: string | null): string {
|
||||
const message = getNLSMessages()?.[index];
|
||||
if (typeof message !== 'string') {
|
||||
if (typeof fallback === 'string') {
|
||||
return fallback;
|
||||
}
|
||||
throw new Error(`!!! NLS MISSING: ${index} !!!`);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a string to be localized. Returns an {@linkcode ILocalizedString}
|
||||
* which contains the localized string and the original string.
|
||||
*
|
||||
* @param info The {@linkcode ILocalizeInfo} which describes the id and comments associated with the localized string.
|
||||
* @param message The string to localize
|
||||
* @param args The arguments to the string
|
||||
*
|
||||
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
|
||||
* @example `localize2({ key: 'sayHello', comment: ['Welcomes user'] }, 'hello {0}', name)`
|
||||
*
|
||||
* @returns ILocalizedString which contains the localized string and the original string.
|
||||
*/
|
||||
export function localize2(info: ILocalizeInfo, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
|
||||
|
||||
/**
|
||||
* Marks a string to be localized. Returns an {@linkcode ILocalizedString}
|
||||
* which contains the localized string and the original string.
|
||||
*
|
||||
* @param key The key to use for localizing the string
|
||||
* @param message The string to localize
|
||||
* @param args The arguments to the string
|
||||
*
|
||||
* @note `message` can contain `{n}` notation where it is replaced by the nth value in `...args`
|
||||
* @example `localize('sayHello', 'hello {0}', name)`
|
||||
*
|
||||
* @returns ILocalizedString which contains the localized string and the original string.
|
||||
*/
|
||||
export function localize2(key: string, message: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString;
|
||||
|
||||
/**
|
||||
* @skipMangle
|
||||
*/
|
||||
export function localize2(data: ILocalizeInfo | string /* | number when built */, originalMessage: string, ...args: (string | number | boolean | undefined | null)[]): ILocalizedString {
|
||||
let message: string;
|
||||
if (typeof data === 'number') {
|
||||
message = lookupMessage(data, originalMessage);
|
||||
} else {
|
||||
message = originalMessage;
|
||||
}
|
||||
|
||||
const value = _format(message, args);
|
||||
|
||||
return {
|
||||
value,
|
||||
original: originalMessage === message ? value : _format(originalMessage, args)
|
||||
};
|
||||
}
|
||||
|
||||
export interface INLSLanguagePackConfiguration {
|
||||
|
||||
/**
|
||||
* The path to the translations config file that contains pointers to
|
||||
* all message bundles for `main` and extensions.
|
||||
*/
|
||||
readonly translationsConfigFile: string;
|
||||
|
||||
/**
|
||||
* The path to the file containing the translations for this language
|
||||
* pack as flat string array.
|
||||
*/
|
||||
readonly messagesFile: string;
|
||||
|
||||
/**
|
||||
* The path to the file that can be used to signal a corrupt language
|
||||
* pack, for example when reading the `messagesFile` fails. This will
|
||||
* instruct the application to re-create the cache on next startup.
|
||||
*/
|
||||
readonly corruptMarkerFile: string;
|
||||
}
|
||||
|
||||
export interface INLSConfiguration {
|
||||
|
||||
/**
|
||||
* Locale as defined in `argv.json` or `app.getLocale()`.
|
||||
*/
|
||||
readonly userLocale: string;
|
||||
|
||||
/**
|
||||
* Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`).
|
||||
*/
|
||||
readonly osLocale: string;
|
||||
|
||||
/**
|
||||
* The actual language of the UI that ends up being used considering `userLocale`
|
||||
* and `osLocale`.
|
||||
*/
|
||||
readonly resolvedLanguage: string;
|
||||
|
||||
/**
|
||||
* Defined if a language pack is used that is not the
|
||||
* default english language pack. This requires a language
|
||||
* pack to be installed as extension.
|
||||
*/
|
||||
readonly languagePack?: INLSLanguagePackConfiguration;
|
||||
|
||||
/**
|
||||
* The path to the file containing the default english messages
|
||||
* as flat string array. The file is only present in built
|
||||
* versions of the application.
|
||||
*/
|
||||
readonly defaultMessagesFile: string;
|
||||
|
||||
/**
|
||||
* Below properties are deprecated and only there to continue support
|
||||
* for `vscode-nls` module that depends on them.
|
||||
* Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46
|
||||
*/
|
||||
/** @deprecated */
|
||||
readonly locale: string;
|
||||
/** @deprecated */
|
||||
readonly availableLanguages: Record<string, string>;
|
||||
/** @deprecated */
|
||||
readonly _languagePackSupport?: boolean;
|
||||
/** @deprecated */
|
||||
readonly _languagePackId?: string;
|
||||
/** @deprecated */
|
||||
readonly _translationsConfigFile?: string;
|
||||
/** @deprecated */
|
||||
readonly _cacheRoot?: string;
|
||||
/** @deprecated */
|
||||
readonly _resolvedLanguagePackCoreLocation?: string;
|
||||
/** @deprecated */
|
||||
readonly _corruptedFile?: string;
|
||||
}
|
||||
|
||||
export interface ILanguagePack {
|
||||
readonly hash: string;
|
||||
readonly label: string | undefined;
|
||||
readonly extensions: {
|
||||
readonly extensionIdentifier: { readonly id: string; readonly uuid?: string };
|
||||
readonly version: string;
|
||||
}[];
|
||||
readonly translations: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export type ILanguagePacks = Record<string, ILanguagePack | undefined>;
|
||||
275
packages/core/src/objects.ts
Normal file
275
packages/core/src/objects.ts
Normal file
@ -0,0 +1,275 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isTypedArray, isObject, isUndefinedOrNull } from './primitives.js';
|
||||
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
if (obj instanceof RegExp) {
|
||||
return obj;
|
||||
}
|
||||
const result: any = Array.isArray(obj) ? [] : {};
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
result[key] = value && typeof value === 'object' ? deepClone(value) : value;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function deepFreeze<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
const stack: any[] = [obj];
|
||||
while (stack.length > 0) {
|
||||
const obj = stack.shift();
|
||||
Object.freeze(obj);
|
||||
for (const key in obj) {
|
||||
if (_hasOwnProperty.call(obj, key)) {
|
||||
const prop = obj[key];
|
||||
if (typeof prop === 'object' && !Object.isFrozen(prop) && !isTypedArray(prop)) {
|
||||
stack.push(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const _hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
|
||||
export function cloneAndChange(obj: any, changer: (orig: any) => any): any {
|
||||
return _cloneAndChange(obj, changer, new Set());
|
||||
}
|
||||
|
||||
function _cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set<any>): any {
|
||||
if (isUndefinedOrNull(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const changed = changer(obj);
|
||||
if (typeof changed !== 'undefined') {
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
const r1: any[] = [];
|
||||
for (const e of obj) {
|
||||
r1.push(_cloneAndChange(e, changer, seen));
|
||||
}
|
||||
return r1;
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
if (seen.has(obj)) {
|
||||
throw new Error('Cannot clone recursive data-structure');
|
||||
}
|
||||
seen.add(obj);
|
||||
const r2 = {};
|
||||
for (const i2 in obj) {
|
||||
if (_hasOwnProperty.call(obj, i2)) {
|
||||
(r2 as any)[i2] = _cloneAndChange(obj[i2], changer, seen);
|
||||
}
|
||||
}
|
||||
seen.delete(obj);
|
||||
return r2;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all properties of source into destination. The optional parameter "overwrite" allows to control
|
||||
* if existing properties on the destination should be overwritten or not. Defaults to true (overwrite).
|
||||
*/
|
||||
export function mixin(destination: any, source: any, overwrite: boolean = true): any {
|
||||
if (!isObject(destination)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (isObject(source)) {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (key in destination) {
|
||||
if (overwrite) {
|
||||
if (isObject(destination[key]) && isObject(source[key])) {
|
||||
mixin(destination[key], source[key], overwrite);
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination[key] = source[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export function equals(one: any, other: any): boolean {
|
||||
if (one === other) {
|
||||
return true;
|
||||
}
|
||||
if (one === null || one === undefined || other === null || other === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof one !== typeof other) {
|
||||
return false;
|
||||
}
|
||||
if (typeof one !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if ((Array.isArray(one)) !== (Array.isArray(other))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i: number;
|
||||
let key: string;
|
||||
|
||||
if (Array.isArray(one)) {
|
||||
if (one.length !== other.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < one.length; i++) {
|
||||
if (!equals(one[i], other[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const oneKeys: string[] = [];
|
||||
|
||||
for (key in one) {
|
||||
oneKeys.push(key);
|
||||
}
|
||||
oneKeys.sort();
|
||||
const otherKeys: string[] = [];
|
||||
for (key in other) {
|
||||
otherKeys.push(key);
|
||||
}
|
||||
otherKeys.sort();
|
||||
if (!equals(oneKeys, otherKeys)) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < oneKeys.length; i++) {
|
||||
if (!equals(one[oneKeys[i]], other[oneKeys[i]])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `JSON.Stringify` with a replacer to break apart any circular references.
|
||||
* This prevents `JSON`.stringify` from throwing the exception
|
||||
* "Uncaught TypeError: Converting circular structure to JSON"
|
||||
*/
|
||||
export function safeStringify(obj: any): string {
|
||||
const seen = new Set<any>();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (isObject(value) || Array.isArray(value)) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular]';
|
||||
} else {
|
||||
seen.add(value);
|
||||
}
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return `[BigInt ${value.toString()}]`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
type obj = { [key: string]: any };
|
||||
/**
|
||||
* Returns an object that has keys for each value that is different in the base object. Keys
|
||||
* that do not exist in the target but in the base object are not considered.
|
||||
*
|
||||
* Note: This is not a deep-diffing method, so the values are strictly taken into the resulting
|
||||
* object if they differ.
|
||||
*
|
||||
* @param base the object to diff against
|
||||
* @param obj the object to use for diffing
|
||||
*/
|
||||
export function distinct(base: obj, target: obj): obj {
|
||||
const result = Object.create(null);
|
||||
|
||||
if (!base || !target) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const targetKeys = Object.keys(target);
|
||||
targetKeys.forEach(k => {
|
||||
const baseValue = base[k];
|
||||
const targetValue = target[k];
|
||||
|
||||
if (!equals(baseValue, targetValue)) {
|
||||
result[k] = targetValue;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getCaseInsensitive(target: obj, key: string): unknown {
|
||||
const lowercaseKey = key.toLowerCase();
|
||||
const equivalentKey = Object.keys(target).find(k => k.toLowerCase() === lowercaseKey);
|
||||
return equivalentKey ? target[equivalentKey] : target[key];
|
||||
}
|
||||
|
||||
export function filter(obj: obj, predicate: (key: string, value: any) => boolean): obj {
|
||||
const result = Object.create(null);
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (predicate(key, value)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getAllPropertyNames(obj: object): string[] {
|
||||
let res: string[] = [];
|
||||
while (Object.prototype !== obj) {
|
||||
res = res.concat(Object.getOwnPropertyNames(obj));
|
||||
obj = Object.getPrototypeOf(obj);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getAllMethodNames(obj: object): string[] {
|
||||
const methods: string[] = [];
|
||||
for (const prop of getAllPropertyNames(obj)) {
|
||||
if (typeof (obj as any)[prop] === 'function') {
|
||||
methods.push(prop);
|
||||
}
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
export function createProxyObject<T extends object>(methodNames: string[], invoke: (method: string, args: unknown[]) => unknown): T {
|
||||
const createProxyMethod = (method: string): () => unknown => {
|
||||
return function () {
|
||||
const args = Array.prototype.slice.call(arguments, 0);
|
||||
return invoke(method, args);
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
const result = {} as T;
|
||||
for (const methodName of methodNames) {
|
||||
(<any>result)[methodName] = createProxyMethod(methodName);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mapValues<T extends {}, R>(obj: T, fn: (value: T[keyof T], key: string) => R): { [K in keyof T]: R } {
|
||||
const result: { [key: string]: R } = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[key] = fn(<T[keyof T]>value, key);
|
||||
}
|
||||
return result as { [K in keyof T]: R };
|
||||
}
|
||||
8
packages/core/src/observable.ts
Normal file
8
packages/core/src/observable.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This is a facade for the observable implementation. Only import from here!
|
||||
|
||||
export * from './observableInternal/index.js';
|
||||
30
packages/core/src/observableInternal/api.ts
Normal file
30
packages/core/src/observableInternal/api.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISettableObservable, ObservableValue } from './base.js';
|
||||
import { DebugNameData, IDebugNameData } from './debugName.js';
|
||||
import { EqualityComparer, strictEquals } from './commonFacade/deps.js';
|
||||
import { LazyObservableValue } from './lazyObservableValue.js';
|
||||
|
||||
export function observableValueOpts<T, TChange = void>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
lazy?: boolean;
|
||||
},
|
||||
initialValue: T
|
||||
): ISettableObservable<T, TChange> {
|
||||
if (options.lazy) {
|
||||
return new LazyObservableValue(
|
||||
new DebugNameData(options.owner, options.debugName, undefined),
|
||||
initialValue,
|
||||
options.equalsFn ?? strictEquals,
|
||||
);
|
||||
}
|
||||
return new ObservableValue(
|
||||
new DebugNameData(options.owner, options.debugName, undefined),
|
||||
initialValue,
|
||||
options.equalsFn ?? strictEquals,
|
||||
);
|
||||
}
|
||||
327
packages/core/src/observableInternal/autorun.ts
Normal file
327
packages/core/src/observableInternal/autorun.ts
Normal file
@ -0,0 +1,327 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChangeContext, IObservable, IObservableWithChange, IObserver, IReader } from './base.js';
|
||||
import { DebugNameData, IDebugNameData } from './debugName.js';
|
||||
import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js';
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
||||
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
||||
*/
|
||||
export function autorun(fn: (reader: IReader) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(undefined, undefined, fn),
|
||||
fn,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
||||
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
||||
*/
|
||||
export function autorunOpts(options: IDebugNameData & {}, fn: (reader: IReader) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
||||
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
||||
*
|
||||
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `handleChange` to add a reported change to the change summary.
|
||||
* The run function is given the last change summary.
|
||||
* The change summary is discarded after the run function was called.
|
||||
*
|
||||
* @see autorun
|
||||
*/
|
||||
export function autorunHandleChanges<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
createEmptyChangeSummary?: () => TChangeSummary;
|
||||
handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary) => void
|
||||
): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
options.createEmptyChangeSummary,
|
||||
options.handleChange
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
|
||||
*/
|
||||
export function autorunWithStoreHandleChanges<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
createEmptyChangeSummary?: () => TChangeSummary;
|
||||
handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void
|
||||
): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = autorunHandleChanges(
|
||||
{
|
||||
owner: options.owner,
|
||||
debugName: options.debugName,
|
||||
debugReferenceFn: options.debugReferenceFn ?? fn,
|
||||
createEmptyChangeSummary: options.createEmptyChangeSummary,
|
||||
handleChange: options.handleChange,
|
||||
},
|
||||
(reader, changeSummary) => {
|
||||
store.clear();
|
||||
fn(reader, changeSummary, store);
|
||||
}
|
||||
);
|
||||
return toDisposable(() => {
|
||||
disposable.dispose();
|
||||
store.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @see autorun (but with a disposable store that is cleared before the next run or on dispose)
|
||||
*/
|
||||
export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = autorunOpts(
|
||||
{
|
||||
owner: undefined,
|
||||
debugName: undefined,
|
||||
debugReferenceFn: fn,
|
||||
},
|
||||
reader => {
|
||||
store.clear();
|
||||
fn(reader, store);
|
||||
}
|
||||
);
|
||||
return toDisposable(() => {
|
||||
disposable.dispose();
|
||||
store.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
export function autorunDelta<T>(
|
||||
observable: IObservable<T>,
|
||||
handler: (args: { lastValue: T | undefined; newValue: T }) => void
|
||||
): IDisposable {
|
||||
let _lastValue: T | undefined;
|
||||
return autorunOpts({ debugReferenceFn: handler }, (reader) => {
|
||||
const newValue = observable.read(reader);
|
||||
const lastValue = _lastValue;
|
||||
_lastValue = newValue;
|
||||
handler({ lastValue, newValue });
|
||||
});
|
||||
}
|
||||
|
||||
export function autorunIterableDelta<T>(
|
||||
getValue: (reader: IReader) => Iterable<T>,
|
||||
handler: (args: { addedValues: T[]; removedValues: T[] }) => void,
|
||||
getUniqueIdentifier: (value: T) => unknown = v => v,
|
||||
) {
|
||||
const lastValues = new Map<unknown, T>();
|
||||
return autorunOpts({ debugReferenceFn: getValue }, (reader) => {
|
||||
const newValues = new Map();
|
||||
const removedValues = new Map(lastValues);
|
||||
for (const value of getValue(reader)) {
|
||||
const id = getUniqueIdentifier(value);
|
||||
if (lastValues.has(id)) {
|
||||
removedValues.delete(id);
|
||||
} else {
|
||||
newValues.set(id, value);
|
||||
lastValues.set(id, value);
|
||||
}
|
||||
}
|
||||
for (const id of removedValues.keys()) {
|
||||
lastValues.delete(id);
|
||||
}
|
||||
|
||||
if (newValues.size || removedValues.size) {
|
||||
handler({ addedValues: [...newValues.values()], removedValues: [...removedValues.values()] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
const enum AutorunState {
|
||||
/**
|
||||
* A dependency could have changed.
|
||||
* We need to explicitly ask them if at least one dependency changed.
|
||||
*/
|
||||
dependenciesMightHaveChanged = 1,
|
||||
|
||||
/**
|
||||
* A dependency changed and we need to recompute.
|
||||
*/
|
||||
stale = 2,
|
||||
upToDate = 3,
|
||||
}
|
||||
|
||||
export class AutorunObserver<TChangeSummary = any> implements IObserver, IReader, IDisposable {
|
||||
private state = AutorunState.stale;
|
||||
private updateCount = 0;
|
||||
private disposed = false;
|
||||
private dependencies = new Set<IObservable<any>>();
|
||||
private dependenciesToBeRemoved = new Set<IObservable<any>>();
|
||||
private changeSummary: TChangeSummary | undefined;
|
||||
|
||||
public get debugName(): string {
|
||||
return this._debugNameData.getDebugName(this) ?? '(anonymous)';
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly _debugNameData: DebugNameData,
|
||||
public readonly _runFn: (reader: IReader, changeSummary: TChangeSummary) => void,
|
||||
private readonly createChangeSummary: (() => TChangeSummary) | undefined,
|
||||
private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
|
||||
) {
|
||||
this.changeSummary = this.createChangeSummary?.();
|
||||
getLogger()?.handleAutorunCreated(this);
|
||||
this._runIfNeeded();
|
||||
|
||||
trackDisposable(this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposed = true;
|
||||
for (const o of this.dependencies) {
|
||||
o.removeObserver(this);
|
||||
}
|
||||
this.dependencies.clear();
|
||||
|
||||
markAsDisposed(this);
|
||||
}
|
||||
|
||||
private _runIfNeeded() {
|
||||
if (this.state === AutorunState.upToDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emptySet = this.dependenciesToBeRemoved;
|
||||
this.dependenciesToBeRemoved = this.dependencies;
|
||||
this.dependencies = emptySet;
|
||||
|
||||
this.state = AutorunState.upToDate;
|
||||
|
||||
const isDisposed = this.disposed;
|
||||
try {
|
||||
if (!isDisposed) {
|
||||
getLogger()?.handleAutorunTriggered(this);
|
||||
const changeSummary = this.changeSummary!;
|
||||
try {
|
||||
this.changeSummary = this.createChangeSummary?.();
|
||||
this._isReaderValid = true;
|
||||
this._runFn(this, changeSummary);
|
||||
} catch (e) {
|
||||
onBugIndicatingError(e);
|
||||
} finally {
|
||||
this._isReaderValid = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!isDisposed) {
|
||||
getLogger()?.handleAutorunFinished(this);
|
||||
}
|
||||
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
|
||||
// Thus, we only unsubscribe from observables that are definitely not read anymore.
|
||||
for (const o of this.dependenciesToBeRemoved) {
|
||||
o.removeObserver(this);
|
||||
}
|
||||
this.dependenciesToBeRemoved.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `Autorun<${this.debugName}>`;
|
||||
}
|
||||
|
||||
// IObserver implementation
|
||||
public beginUpdate(): void {
|
||||
if (this.state === AutorunState.upToDate) {
|
||||
this.state = AutorunState.dependenciesMightHaveChanged;
|
||||
}
|
||||
this.updateCount++;
|
||||
}
|
||||
|
||||
public endUpdate(): void {
|
||||
try {
|
||||
if (this.updateCount === 1) {
|
||||
do {
|
||||
if (this.state === AutorunState.dependenciesMightHaveChanged) {
|
||||
this.state = AutorunState.upToDate;
|
||||
for (const d of this.dependencies) {
|
||||
d.reportChanges();
|
||||
if (this.state as AutorunState === AutorunState.stale) {
|
||||
// The other dependencies will refresh on demand
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._runIfNeeded();
|
||||
} while (this.state !== AutorunState.upToDate);
|
||||
}
|
||||
} finally {
|
||||
this.updateCount--;
|
||||
}
|
||||
|
||||
assertFn(() => this.updateCount >= 0);
|
||||
}
|
||||
|
||||
public handlePossibleChange(observable: IObservable<any>): void {
|
||||
if (this.state === AutorunState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
||||
this.state = AutorunState.dependenciesMightHaveChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
|
||||
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
||||
try {
|
||||
const shouldReact = this._handleChange ? this._handleChange({
|
||||
changedObservable: observable,
|
||||
change,
|
||||
didChange: (o): this is any => o === observable as any,
|
||||
}, this.changeSummary!) : true;
|
||||
if (shouldReact) {
|
||||
this.state = AutorunState.stale;
|
||||
}
|
||||
} catch (e) {
|
||||
onBugIndicatingError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IReader implementation
|
||||
private _isReaderValid = false;
|
||||
|
||||
public readObservable<T>(observable: IObservable<T>): T {
|
||||
if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); }
|
||||
|
||||
// In case the run action disposes the autorun
|
||||
if (this.disposed) {
|
||||
return observable.get();
|
||||
}
|
||||
|
||||
observable.addObserver(this);
|
||||
const value = observable.get();
|
||||
this.dependencies.add(observable);
|
||||
this.dependenciesToBeRemoved.delete(observable);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace autorun {
|
||||
export const Observer = AutorunObserver;
|
||||
}
|
||||
522
packages/core/src/observableInternal/base.ts
Normal file
522
packages/core/src/observableInternal/base.ts
Normal file
@ -0,0 +1,522 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js';
|
||||
import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js';
|
||||
import type { derivedOpts } from './derived.js';
|
||||
import { getLogger, logObservable } from './logging.js';
|
||||
import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js';
|
||||
|
||||
/**
|
||||
* Represents an observable value.
|
||||
*
|
||||
* @template T The type of the values the observable can hold.
|
||||
*/
|
||||
export interface IObservable<T> extends IObservableWithChange<T, unknown> { }
|
||||
|
||||
/**
|
||||
* Represents an observable value.
|
||||
*
|
||||
* @template T The type of the values the observable can hold.
|
||||
* @template TChange The type used to describe value changes
|
||||
* (usually `void` and only used in advanced scenarios).
|
||||
* While observers can miss temporary values of an observable,
|
||||
* they will receive all change values (as long as they are subscribed)!
|
||||
*/
|
||||
export interface IObservableWithChange<T, TChange = unknown> {
|
||||
/**
|
||||
* Returns the current value.
|
||||
*
|
||||
* Calls {@link IObserver.handleChange} if the observable notices that the value changed.
|
||||
* Must not be called from {@link IObserver.handleChange}!
|
||||
*/
|
||||
get(): T;
|
||||
|
||||
/**
|
||||
* Forces the observable to check for changes and report them.
|
||||
*
|
||||
* Has the same effect as calling {@link IObservable.get}, but does not force the observable
|
||||
* to actually construct the value, e.g. if change deltas are used.
|
||||
* Calls {@link IObserver.handleChange} if the observable notices that the value changed.
|
||||
* Must not be called from {@link IObserver.handleChange}!
|
||||
*/
|
||||
reportChanges(): void;
|
||||
|
||||
/**
|
||||
* Adds the observer to the set of subscribed observers.
|
||||
* This method is idempotent.
|
||||
*/
|
||||
addObserver(observer: IObserver): void;
|
||||
|
||||
/**
|
||||
* Removes the observer from the set of subscribed observers.
|
||||
* This method is idempotent.
|
||||
*/
|
||||
removeObserver(observer: IObserver): void;
|
||||
|
||||
/**
|
||||
* Reads the current value and subscribes the reader to this observable.
|
||||
*
|
||||
* Calls {@link IReader.readObservable} if a reader is given, otherwise {@link IObservable.get}
|
||||
* (see {@link ConvenientObservable.read} for the implementation).
|
||||
*/
|
||||
read(reader: IReader | undefined): T;
|
||||
|
||||
/**
|
||||
* Creates a derived observable that depends on this observable.
|
||||
* Use the reader to read other observables
|
||||
* (see {@link ConvenientObservable.map} for the implementation).
|
||||
*/
|
||||
map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
map<TNew>(owner: object, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
|
||||
flatten<TNew>(this: IObservable<IObservable<TNew>>): IObservable<TNew>;
|
||||
|
||||
/**
|
||||
* ONLY FOR DEBUGGING!
|
||||
* Logs computations of this derived.
|
||||
*/
|
||||
log(): IObservableWithChange<T, TChange>;
|
||||
|
||||
/**
|
||||
* Makes sure this value is computed eagerly.
|
||||
*/
|
||||
recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T>;
|
||||
|
||||
/**
|
||||
* Makes sure this value is cached.
|
||||
*/
|
||||
keepObserved(store: DisposableStore): IObservable<T>;
|
||||
|
||||
/**
|
||||
* A human-readable name for debugging purposes.
|
||||
*/
|
||||
readonly debugName: string;
|
||||
|
||||
/**
|
||||
* This property captures the type of the change object. Do not use it at runtime!
|
||||
*/
|
||||
readonly TChange: TChange;
|
||||
}
|
||||
|
||||
export interface IReader {
|
||||
/**
|
||||
* Reads the value of an observable and subscribes to it.
|
||||
*/
|
||||
readObservable<T>(observable: IObservableWithChange<T, any>): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an observer that can be subscribed to an observable.
|
||||
*
|
||||
* If an observer is subscribed to an observable and that observable didn't signal
|
||||
* a change through one of the observer methods, the observer can assume that the
|
||||
* observable didn't change.
|
||||
* If an observable reported a possible change, {@link IObservable.reportChanges} forces
|
||||
* the observable to report an actual change if there was one.
|
||||
*/
|
||||
export interface IObserver {
|
||||
/**
|
||||
* Signals that the given observable might have changed and a transaction potentially modifying that observable started.
|
||||
* Before the given observable can call this method again, is must call {@link IObserver.endUpdate}.
|
||||
*
|
||||
* Implementations must not get/read the value of other observables, as they might not have received this event yet!
|
||||
* The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
|
||||
*/
|
||||
beginUpdate<T>(observable: IObservable<T>): void;
|
||||
|
||||
/**
|
||||
* Signals that the transaction that potentially modified the given observable ended.
|
||||
* This is a good place to react to (potential) changes.
|
||||
*/
|
||||
endUpdate<T>(observable: IObservable<T>): void;
|
||||
|
||||
/**
|
||||
* Signals that the given observable might have changed.
|
||||
* The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
|
||||
*
|
||||
* Implementations must not get/read the value of other observables, as they might not have received this event yet!
|
||||
* The change should be processed lazily or in {@link IObserver.endUpdate}.
|
||||
*/
|
||||
handlePossibleChange<T>(observable: IObservable<T>): void;
|
||||
|
||||
/**
|
||||
* Signals that the given {@link observable} changed.
|
||||
*
|
||||
* Implementations must not get/read the value of other observables, as they might not have received this event yet!
|
||||
* The change should be processed lazily or in {@link IObserver.endUpdate}.
|
||||
*
|
||||
* @param change Indicates how or why the value changed.
|
||||
*/
|
||||
handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void;
|
||||
}
|
||||
|
||||
export interface ISettable<T, TChange = void> {
|
||||
/**
|
||||
* Sets the value of the observable.
|
||||
* Use a transaction to batch multiple changes (with a transaction, observers only react at the end of the transaction).
|
||||
*
|
||||
* @param transaction When given, value changes are handled on demand or when the transaction ends.
|
||||
* @param change Describes how or why the value changed.
|
||||
*/
|
||||
set(value: T, transaction: ITransaction | undefined, change: TChange): void;
|
||||
}
|
||||
|
||||
export interface ITransaction {
|
||||
/**
|
||||
* Calls {@link Observer.beginUpdate} immediately
|
||||
* and {@link Observer.endUpdate} when the transaction ends.
|
||||
*/
|
||||
updateObserver(observer: IObserver, observable: IObservableWithChange<any, any>): void;
|
||||
}
|
||||
|
||||
let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange;
|
||||
export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange: typeof _recomputeInitiallyAndOnChange) {
|
||||
_recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
|
||||
}
|
||||
|
||||
let _keepObserved: typeof keepObserved;
|
||||
export function _setKeepObserved(keepObserved: typeof _keepObserved) {
|
||||
_keepObserved = keepObserved;
|
||||
}
|
||||
|
||||
|
||||
let _derived: typeof derivedOpts;
|
||||
/**
|
||||
* @internal
|
||||
* This is to allow splitting files.
|
||||
*/
|
||||
export function _setDerivedOpts(derived: typeof _derived) {
|
||||
_derived = derived;
|
||||
}
|
||||
|
||||
export abstract class ConvenientObservable<T, TChange> implements IObservableWithChange<T, TChange> {
|
||||
get TChange(): TChange { return null!; }
|
||||
|
||||
public abstract get(): T;
|
||||
|
||||
public reportChanges(): void {
|
||||
this.get();
|
||||
}
|
||||
|
||||
public abstract addObserver(observer: IObserver): void;
|
||||
public abstract removeObserver(observer: IObserver): void;
|
||||
|
||||
/** @sealed */
|
||||
public read(reader: IReader | undefined): T {
|
||||
if (reader) {
|
||||
return reader.readObservable(this);
|
||||
} else {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
/** @sealed */
|
||||
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
|
||||
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
|
||||
const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : fnOrUndefined;
|
||||
|
||||
return _derived(
|
||||
{
|
||||
owner,
|
||||
debugName: () => {
|
||||
const name = getFunctionName(fn);
|
||||
if (name !== undefined) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// regexp to match `x => x.y` or `x => x?.y` where x and y can be arbitrary identifiers (uses backref):
|
||||
const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1(?:\??)\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/;
|
||||
const match = regexp.exec(fn.toString());
|
||||
if (match) {
|
||||
return `${this.debugName}.${match[2]}`;
|
||||
}
|
||||
if (!owner) {
|
||||
return `${this.debugName} (mapped)`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
debugReferenceFn: fn,
|
||||
},
|
||||
(reader) => fn(this.read(reader), reader),
|
||||
);
|
||||
}
|
||||
|
||||
public log(): IObservableWithChange<T, TChange> {
|
||||
logObservable(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @sealed
|
||||
* Converts an observable of an observable value into a direct observable of the value.
|
||||
*/
|
||||
public flatten<TNew>(this: IObservable<IObservableWithChange<TNew, any>>): IObservable<TNew> {
|
||||
return _derived(
|
||||
{
|
||||
owner: undefined,
|
||||
debugName: () => `${this.debugName} (flattened)`,
|
||||
},
|
||||
(reader) => this.read(reader).read(reader),
|
||||
);
|
||||
}
|
||||
|
||||
public recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T> {
|
||||
store.add(_recomputeInitiallyAndOnChange!(this, handleValue));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that this observable is observed. This keeps the cache alive.
|
||||
* However, in case of deriveds, it does not force eager evaluation (only when the value is read/get).
|
||||
* Use `recomputeInitiallyAndOnChange` for eager evaluation.
|
||||
*/
|
||||
public keepObserved(store: DisposableStore): IObservable<T> {
|
||||
store.add(_keepObserved!(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract get debugName(): string;
|
||||
|
||||
protected get debugValue() {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
|
||||
protected readonly observers = new Set<IObserver>();
|
||||
|
||||
public addObserver(observer: IObserver): void {
|
||||
const len = this.observers.size;
|
||||
this.observers.add(observer);
|
||||
if (len === 0) {
|
||||
this.onFirstObserverAdded();
|
||||
}
|
||||
}
|
||||
|
||||
public removeObserver(observer: IObserver): void {
|
||||
const deleted = this.observers.delete(observer);
|
||||
if (deleted && this.observers.size === 0) {
|
||||
this.onLastObserverRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
protected onFirstObserverAdded(): void { }
|
||||
protected onLastObserverRemoved(): void { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transaction in which many observables can be changed at once.
|
||||
* {@link fn} should start with a JS Doc using `@description` to give the transaction a debug name.
|
||||
* Reaction run on demand or when the transaction ends.
|
||||
*/
|
||||
|
||||
export function transaction(fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
|
||||
let _globalTransaction: ITransaction | undefined = undefined;
|
||||
|
||||
export function globalTransaction(fn: (tx: ITransaction) => void) {
|
||||
if (_globalTransaction) {
|
||||
fn(_globalTransaction);
|
||||
} else {
|
||||
const tx = new TransactionImpl(fn, undefined);
|
||||
_globalTransaction = tx;
|
||||
try {
|
||||
fn(tx);
|
||||
} finally {
|
||||
tx.finish(); // During finish, more actions might be added to the transaction.
|
||||
// Which is why we only clear the global transaction after finish.
|
||||
_globalTransaction = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function asyncTransaction(fn: (tx: ITransaction) => Promise<void>, getDebugName?: () => string): Promise<void> {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
await fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to chain transactions.
|
||||
*/
|
||||
export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
if (!tx) {
|
||||
transaction(fn, getDebugName);
|
||||
} else {
|
||||
fn(tx);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionImpl implements ITransaction {
|
||||
private updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
|
||||
|
||||
constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) {
|
||||
getLogger()?.handleBeginTransaction(this);
|
||||
}
|
||||
|
||||
public getDebugName(): string | undefined {
|
||||
if (this._getDebugName) {
|
||||
return this._getDebugName();
|
||||
}
|
||||
return getFunctionName(this._fn);
|
||||
}
|
||||
|
||||
public updateObserver(observer: IObserver, observable: IObservable<any>): void {
|
||||
// When this gets called while finish is active, they will still get considered
|
||||
this.updatingObservers!.push({ observer, observable });
|
||||
observer.beginUpdate(observable);
|
||||
}
|
||||
|
||||
public finish(): void {
|
||||
const updatingObservers = this.updatingObservers!;
|
||||
for (let i = 0; i < updatingObservers.length; i++) {
|
||||
const { observer, observable } = updatingObservers[i];
|
||||
observer.endUpdate(observable);
|
||||
}
|
||||
// Prevent anyone from updating observers from now on.
|
||||
this.updatingObservers = null;
|
||||
getLogger()?.handleEndTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A settable observable.
|
||||
*/
|
||||
export interface ISettableObservable<T, TChange = void> extends IObservableWithChange<T, TChange>, ISettable<T, TChange> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable value.
|
||||
* Observers get informed when the value changes.
|
||||
* @template TChange An arbitrary type to describe how or why the value changed. Defaults to `void`.
|
||||
* Observers will receive every single change value.
|
||||
*/
|
||||
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> {
|
||||
let debugNameData: DebugNameData;
|
||||
if (typeof nameOrOwner === 'string') {
|
||||
debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
|
||||
} else {
|
||||
debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
|
||||
}
|
||||
return new ObservableValue(debugNameData, initialValue, strictEquals);
|
||||
}
|
||||
|
||||
export class ObservableValue<T, TChange = void>
|
||||
extends BaseObservable<T, TChange>
|
||||
implements ISettableObservable<T, TChange> {
|
||||
protected _value: T;
|
||||
|
||||
get debugName() {
|
||||
return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
initialValue: T,
|
||||
private readonly _equalityComparator: EqualityComparer<T>,
|
||||
) {
|
||||
super();
|
||||
this._value = initialValue;
|
||||
}
|
||||
public override get(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
|
||||
if (change === undefined && this._equalityComparator(this._value, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _tx: TransactionImpl | undefined;
|
||||
if (!tx) {
|
||||
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
|
||||
}
|
||||
try {
|
||||
const oldValue = this._value;
|
||||
this._setValue(value);
|
||||
getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true, hadValue: true });
|
||||
|
||||
for (const observer of this.observers) {
|
||||
tx.updateObserver(observer, this);
|
||||
observer.handleChange(this, change);
|
||||
}
|
||||
} finally {
|
||||
if (_tx) {
|
||||
_tx.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `${this.debugName}: ${this._value}`;
|
||||
}
|
||||
|
||||
protected _setValue(newValue: T): void {
|
||||
this._value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A disposable observable. When disposed, its value is also disposed.
|
||||
* When a new value is set, the previous value is disposed.
|
||||
*/
|
||||
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
|
||||
let debugNameData: DebugNameData;
|
||||
if (typeof nameOrOwner === 'string') {
|
||||
debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
|
||||
} else {
|
||||
debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
|
||||
}
|
||||
return new DisposableObservableValue(debugNameData, initialValue, strictEquals);
|
||||
}
|
||||
|
||||
export class DisposableObservableValue<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
|
||||
protected override _setValue(newValue: T): void {
|
||||
if (this._value === newValue) {
|
||||
return;
|
||||
}
|
||||
if (this._value) {
|
||||
this._value.dispose();
|
||||
}
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._value?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IChangeTracker {
|
||||
/**
|
||||
* Returns if this change should cause an invalidation.
|
||||
* Implementations can record changes.
|
||||
*/
|
||||
handleChange(context: IChangeContext): boolean;
|
||||
}
|
||||
|
||||
export interface IChangeContext {
|
||||
readonly changedObservable: IObservableWithChange<any, any>;
|
||||
readonly change: unknown;
|
||||
|
||||
/**
|
||||
* Returns if the given observable caused the change.
|
||||
*/
|
||||
didChange<T, TChange>(observable: IObservableWithChange<T, TChange>): this is { change: TChange };
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export { CancellationError } from '../../errors.js';
|
||||
export { CancellationToken, CancellationTokenSource } from '../../cancellation.js';
|
||||
10
packages/core/src/observableInternal/commonFacade/deps.ts
Normal file
10
packages/core/src/observableInternal/commonFacade/deps.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export { assertFn } from '../../assert.js';
|
||||
export { type EqualityComparer, strictEquals } from '../../equals.js';
|
||||
export { BugIndicatingError, onBugIndicatingError } from '../../errors.js';
|
||||
export { Event, type IValueWithChangeEvent } from '../../event.js';
|
||||
export { DisposableStore, type IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../../lifecycle.js';
|
||||
145
packages/core/src/observableInternal/debugName.ts
Normal file
145
packages/core/src/observableInternal/debugName.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDebugNameData {
|
||||
/**
|
||||
* The owner object of an observable.
|
||||
* Used for debugging only, such as computing a name for the observable by iterating over the fields of the owner.
|
||||
*/
|
||||
readonly owner?: DebugOwner | undefined;
|
||||
|
||||
/**
|
||||
* A string or function that returns a string that represents the name of the observable.
|
||||
* Used for debugging only.
|
||||
*/
|
||||
readonly debugName?: DebugNameSource | undefined;
|
||||
|
||||
/**
|
||||
* A function that points to the defining function of the object.
|
||||
* Used for debugging only.
|
||||
*/
|
||||
readonly debugReferenceFn?: Function | undefined;
|
||||
}
|
||||
|
||||
export class DebugNameData {
|
||||
constructor(
|
||||
public readonly owner: DebugOwner | undefined,
|
||||
public readonly debugNameSource: DebugNameSource | undefined,
|
||||
public readonly referenceFn: Function | undefined,
|
||||
) { }
|
||||
|
||||
public getDebugName(target: object): string | undefined {
|
||||
return getDebugName(target, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The owning object of an observable.
|
||||
* Is only used for debugging purposes, such as computing a name for the observable by iterating over the fields of the owner.
|
||||
*/
|
||||
export type DebugOwner = object | undefined;
|
||||
export type DebugNameSource = string | (() => string | undefined);
|
||||
|
||||
const countPerName = new Map<string, number>();
|
||||
const cachedDebugName = new WeakMap<object, string>();
|
||||
|
||||
export function getDebugName(target: object, data: DebugNameData): string | undefined {
|
||||
const cached = cachedDebugName.get(target);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const dbgName = computeDebugName(target, data);
|
||||
if (dbgName) {
|
||||
let count = countPerName.get(dbgName) ?? 0;
|
||||
count++;
|
||||
countPerName.set(dbgName, count);
|
||||
const result = count === 1 ? dbgName : `${dbgName}#${count}`;
|
||||
cachedDebugName.set(target, result);
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function computeDebugName(self: object, data: DebugNameData): string | undefined {
|
||||
const cached = cachedDebugName.get(self);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const ownerStr = data.owner ? formatOwner(data.owner) + `.` : '';
|
||||
|
||||
let result: string | undefined;
|
||||
const debugNameSource = data.debugNameSource;
|
||||
if (debugNameSource !== undefined) {
|
||||
if (typeof debugNameSource === 'function') {
|
||||
result = debugNameSource();
|
||||
if (result !== undefined) {
|
||||
return ownerStr + result;
|
||||
}
|
||||
} else {
|
||||
return ownerStr + debugNameSource;
|
||||
}
|
||||
}
|
||||
|
||||
const referenceFn = data.referenceFn;
|
||||
if (referenceFn !== undefined) {
|
||||
result = getFunctionName(referenceFn);
|
||||
if (result !== undefined) {
|
||||
return ownerStr + result;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.owner !== undefined) {
|
||||
const key = findKey(data.owner, self);
|
||||
if (key !== undefined) {
|
||||
return ownerStr + key;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findKey(obj: object, value: object): string | undefined {
|
||||
for (const key in obj) {
|
||||
if ((obj as any)[key] === value) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const countPerClassName = new Map<string, number>();
|
||||
const ownerId = new WeakMap<object, string>();
|
||||
|
||||
function formatOwner(owner: object): string {
|
||||
const id = ownerId.get(owner);
|
||||
if (id) {
|
||||
return id;
|
||||
}
|
||||
const className = getClassName(owner);
|
||||
let count = countPerClassName.get(className) ?? 0;
|
||||
count++;
|
||||
countPerClassName.set(className, count);
|
||||
const result = count === 1 ? className : `${className}#${count}`;
|
||||
ownerId.set(owner, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getClassName(obj: object): string {
|
||||
const ctor = obj.constructor;
|
||||
if (ctor) {
|
||||
return ctor.name;
|
||||
}
|
||||
return 'Object';
|
||||
}
|
||||
|
||||
export function getFunctionName(fn: Function): string | undefined {
|
||||
const fnSrc = fn.toString();
|
||||
// Pattern: /** @description ... */
|
||||
const regexp = /\/\*\*\s*@description\s*([^*]*)\*\//;
|
||||
const match = regexp.exec(fnSrc);
|
||||
const result = match ? match[1] : undefined;
|
||||
return result?.trim();
|
||||
}
|
||||
495
packages/core/src/observableInternal/derived.ts
Normal file
495
packages/core/src/observableInternal/derived.ts
Normal file
@ -0,0 +1,495 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BaseObservable, IChangeContext, IObservable, IObservableWithChange, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js';
|
||||
import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js';
|
||||
import { BugIndicatingError, DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js';
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Creates an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*/
|
||||
export function derived<T>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derived<T>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derived<T>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFn?: ((reader: IReader) => T) | undefined): IObservable<T> {
|
||||
if (computeFn !== undefined) {
|
||||
return new Derived(
|
||||
new DebugNameData(computeFnOrOwner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
return new Derived(
|
||||
new DebugNameData(undefined, undefined, computeFnOrOwner as any),
|
||||
computeFnOrOwner as any,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
|
||||
return new DerivedWithSetter(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals,
|
||||
setter,
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedOpts<T>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
onLastObserverRemoved?: (() => void);
|
||||
},
|
||||
computeFn: (reader: IReader) => T
|
||||
): IObservable<T> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
options.onLastObserverRemoved,
|
||||
options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
_setDerivedOpts(derivedOpts);
|
||||
|
||||
/**
|
||||
* Represents an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*
|
||||
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `handleChange` to add a reported change to the change summary.
|
||||
* The compute function is given the last change summary.
|
||||
* The change summary is discarded after the compute function was called.
|
||||
*
|
||||
* @see derived
|
||||
*/
|
||||
export function derivedHandleChanges<T, TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
createEmptyChangeSummary: () => TChangeSummary;
|
||||
handleChange: (context: IChangeContext, changeSummary: TChangeSummary) => boolean;
|
||||
equalityComparer?: EqualityComparer<T>;
|
||||
},
|
||||
computeFn: (reader: IReader, changeSummary: TChangeSummary) => T
|
||||
): IObservable<T> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, undefined),
|
||||
computeFn,
|
||||
options.createEmptyChangeSummary,
|
||||
options.handleChange,
|
||||
undefined,
|
||||
options.equalityComparer ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
export function derivedWithStore<T>(owner: object, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | object, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: DisposableStore) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
store.clear();
|
||||
return computeFn(r, store);
|
||||
}, undefined,
|
||||
undefined,
|
||||
() => store.dispose(),
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
let store: DisposableStore | undefined = undefined;
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (!store) {
|
||||
store = new DisposableStore();
|
||||
} else {
|
||||
store.clear();
|
||||
}
|
||||
const result = computeFn(r);
|
||||
if (result) {
|
||||
store.add(result);
|
||||
}
|
||||
return result;
|
||||
}, undefined,
|
||||
undefined,
|
||||
() => {
|
||||
if (store) {
|
||||
store.dispose();
|
||||
store = undefined;
|
||||
}
|
||||
},
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
const enum DerivedState {
|
||||
/** Initial state, no previous value, recomputation needed */
|
||||
initial = 0,
|
||||
|
||||
/**
|
||||
* A dependency could have changed.
|
||||
* We need to explicitly ask them if at least one dependency changed.
|
||||
*/
|
||||
dependenciesMightHaveChanged = 1,
|
||||
|
||||
/**
|
||||
* A dependency changed and we need to recompute.
|
||||
* After recomputation, we need to check the previous value to see if we changed as well.
|
||||
*/
|
||||
stale = 2,
|
||||
|
||||
/**
|
||||
* No change reported, our cached value is up to date.
|
||||
*/
|
||||
upToDate = 3,
|
||||
}
|
||||
|
||||
export class Derived<T, TChangeSummary = any> extends BaseObservable<T, void> implements IReader, IObserver {
|
||||
private state = DerivedState.initial;
|
||||
private value: T | undefined = undefined;
|
||||
private updateCount = 0;
|
||||
private dependencies = new Set<IObservable<any>>();
|
||||
private dependenciesToBeRemoved = new Set<IObservable<any>>();
|
||||
private changeSummary: TChangeSummary | undefined = undefined;
|
||||
private _isUpdating = false;
|
||||
private _isComputing = false;
|
||||
|
||||
public override get debugName(): string {
|
||||
return this._debugNameData.getDebugName(this) ?? '(anonymous)';
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly _debugNameData: DebugNameData,
|
||||
public readonly _computeFn: (reader: IReader, changeSummary: TChangeSummary) => T,
|
||||
private readonly createChangeSummary: (() => TChangeSummary) | undefined,
|
||||
private readonly _handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
|
||||
private readonly _handleLastObserverRemoved: (() => void) | undefined = undefined,
|
||||
private readonly _equalityComparator: EqualityComparer<T>,
|
||||
) {
|
||||
super();
|
||||
this.changeSummary = this.createChangeSummary?.();
|
||||
getLogger()?.handleDerivedCreated(this);
|
||||
}
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
/**
|
||||
* We are not tracking changes anymore, thus we have to assume
|
||||
* that our cache is invalid.
|
||||
*/
|
||||
this.state = DerivedState.initial;
|
||||
this.value = undefined;
|
||||
getLogger()?.handleDerivedCleared(this);
|
||||
for (const d of this.dependencies) {
|
||||
d.removeObserver(this);
|
||||
}
|
||||
this.dependencies.clear();
|
||||
|
||||
this._handleLastObserverRemoved?.();
|
||||
}
|
||||
|
||||
public override get(): T {
|
||||
if (this._isComputing) {
|
||||
throw new BugIndicatingError('Cyclic deriveds are not supported yet!');
|
||||
}
|
||||
|
||||
if (this.observers.size === 0) {
|
||||
let result;
|
||||
// Without observers, we don't know when to clean up stuff.
|
||||
// Thus, we don't cache anything to prevent memory leaks.
|
||||
try {
|
||||
this._isReaderValid = true;
|
||||
result = this._computeFn(this, this.createChangeSummary?.()!);
|
||||
} finally {
|
||||
this._isReaderValid = false;
|
||||
}
|
||||
// Clear new dependencies
|
||||
this.onLastObserverRemoved();
|
||||
return result;
|
||||
|
||||
} else {
|
||||
do {
|
||||
// We might not get a notification for a dependency that changed while it is updating,
|
||||
// thus we also have to ask all our depedencies if they changed in this case.
|
||||
if (this.state === DerivedState.dependenciesMightHaveChanged) {
|
||||
for (const d of this.dependencies) {
|
||||
/** might call {@link handleChange} indirectly, which could make us stale */
|
||||
d.reportChanges();
|
||||
|
||||
if (this.state as DerivedState === DerivedState.stale) {
|
||||
// The other dependencies will refresh on demand, so early break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We called report changes of all dependencies.
|
||||
// If we are still not stale, we can assume to be up to date again.
|
||||
if (this.state === DerivedState.dependenciesMightHaveChanged) {
|
||||
this.state = DerivedState.upToDate;
|
||||
}
|
||||
|
||||
this._recomputeIfNeeded();
|
||||
// In case recomputation changed one of our dependencies, we need to recompute again.
|
||||
} while (this.state !== DerivedState.upToDate);
|
||||
return this.value!;
|
||||
}
|
||||
}
|
||||
|
||||
private _recomputeIfNeeded() {
|
||||
if (this.state === DerivedState.upToDate) {
|
||||
return;
|
||||
}
|
||||
const emptySet = this.dependenciesToBeRemoved;
|
||||
this.dependenciesToBeRemoved = this.dependencies;
|
||||
this.dependencies = emptySet;
|
||||
|
||||
const hadValue = this.state !== DerivedState.initial;
|
||||
const oldValue = this.value;
|
||||
this.state = DerivedState.upToDate;
|
||||
|
||||
let didChange = false;
|
||||
|
||||
this._isComputing = false; // TODO@hediet: Set to true and investigate diff editor scrolling issues! (also see test.skip('catches cyclic dependencies')
|
||||
|
||||
try {
|
||||
const changeSummary = this.changeSummary!;
|
||||
this.changeSummary = this.createChangeSummary?.();
|
||||
try {
|
||||
this._isReaderValid = true;
|
||||
/** might call {@link handleChange} indirectly, which could invalidate us */
|
||||
this.value = this._computeFn(this, changeSummary);
|
||||
} finally {
|
||||
this._isReaderValid = false;
|
||||
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
|
||||
// Thus, we only unsubscribe from observables that are definitely not read anymore.
|
||||
for (const o of this.dependenciesToBeRemoved) {
|
||||
o.removeObserver(this);
|
||||
}
|
||||
this.dependenciesToBeRemoved.clear();
|
||||
}
|
||||
|
||||
didChange = hadValue && !(this._equalityComparator(oldValue!, this.value));
|
||||
|
||||
getLogger()?.handleDerivedRecomputed(this, {
|
||||
oldValue,
|
||||
newValue: this.value,
|
||||
change: undefined,
|
||||
didChange,
|
||||
hadValue,
|
||||
});
|
||||
} catch (e) {
|
||||
onBugIndicatingError(e);
|
||||
}
|
||||
|
||||
this._isComputing = false;
|
||||
|
||||
if (didChange) {
|
||||
for (const r of this.observers) {
|
||||
r.handleChange(this, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return `LazyDerived<${this.debugName}>`;
|
||||
}
|
||||
|
||||
// IObserver Implementation
|
||||
|
||||
public beginUpdate<T>(_observable: IObservable<T>): void {
|
||||
if (this._isUpdating) {
|
||||
throw new BugIndicatingError('Cyclic deriveds are not supported yet!');
|
||||
}
|
||||
|
||||
this.updateCount++;
|
||||
this._isUpdating = true;
|
||||
try {
|
||||
const propagateBeginUpdate = this.updateCount === 1;
|
||||
if (this.state === DerivedState.upToDate) {
|
||||
this.state = DerivedState.dependenciesMightHaveChanged;
|
||||
// If we propagate begin update, that will already signal a possible change.
|
||||
if (!propagateBeginUpdate) {
|
||||
for (const r of this.observers) {
|
||||
r.handlePossibleChange(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (propagateBeginUpdate) {
|
||||
for (const r of this.observers) {
|
||||
r.beginUpdate(this); // This signals a possible change
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _removedObserverToCallEndUpdateOn: Set<IObserver> | null = null;
|
||||
|
||||
public endUpdate<T>(_observable: IObservable<T>): void {
|
||||
this.updateCount--;
|
||||
if (this.updateCount === 0) {
|
||||
// End update could change the observer list.
|
||||
const observers = [...this.observers];
|
||||
for (const r of observers) {
|
||||
r.endUpdate(this);
|
||||
}
|
||||
if (this._removedObserverToCallEndUpdateOn) {
|
||||
const observers = [...this._removedObserverToCallEndUpdateOn];
|
||||
this._removedObserverToCallEndUpdateOn = null;
|
||||
for (const r of observers) {
|
||||
r.endUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertFn(() => this.updateCount >= 0);
|
||||
}
|
||||
|
||||
public handlePossibleChange<T>(observable: IObservable<T>): void {
|
||||
// In all other states, observers already know that we might have changed.
|
||||
if (this.state === DerivedState.upToDate && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
||||
this.state = DerivedState.dependenciesMightHaveChanged;
|
||||
for (const r of this.observers) {
|
||||
r.handlePossibleChange(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
|
||||
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
||||
let shouldReact = false;
|
||||
try {
|
||||
shouldReact = this._handleChange ? this._handleChange({
|
||||
changedObservable: observable,
|
||||
change,
|
||||
didChange: (o): this is any => o === observable as any,
|
||||
}, this.changeSummary!) : true;
|
||||
} catch (e) {
|
||||
onBugIndicatingError(e);
|
||||
}
|
||||
|
||||
const wasUpToDate = this.state === DerivedState.upToDate;
|
||||
if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) {
|
||||
this.state = DerivedState.stale;
|
||||
if (wasUpToDate) {
|
||||
for (const r of this.observers) {
|
||||
r.handlePossibleChange(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IReader Implementation
|
||||
private _isReaderValid = false;
|
||||
|
||||
public readObservable<T>(observable: IObservable<T>): T {
|
||||
if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); }
|
||||
|
||||
// Subscribe before getting the value to enable caching
|
||||
observable.addObserver(this);
|
||||
/** This might call {@link handleChange} indirectly, which could invalidate us */
|
||||
const value = observable.get();
|
||||
// Which is why we only add the observable to the dependencies now.
|
||||
this.dependencies.add(observable);
|
||||
this.dependenciesToBeRemoved.delete(observable);
|
||||
return value;
|
||||
}
|
||||
|
||||
public override addObserver(observer: IObserver): void {
|
||||
const shouldCallBeginUpdate = !this.observers.has(observer) && this.updateCount > 0;
|
||||
super.addObserver(observer);
|
||||
|
||||
if (shouldCallBeginUpdate) {
|
||||
if (this._removedObserverToCallEndUpdateOn && this._removedObserverToCallEndUpdateOn.has(observer)) {
|
||||
this._removedObserverToCallEndUpdateOn.delete(observer);
|
||||
} else {
|
||||
observer.beginUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override removeObserver(observer: IObserver): void {
|
||||
if (this.observers.has(observer) && this.updateCount > 0) {
|
||||
if (!this._removedObserverToCallEndUpdateOn) {
|
||||
this._removedObserverToCallEndUpdateOn = new Set();
|
||||
}
|
||||
this._removedObserverToCallEndUpdateOn.add(observer);
|
||||
}
|
||||
super.removeObserver(observer);
|
||||
}
|
||||
|
||||
public override log(): IObservableWithChange<T, void> {
|
||||
if (!getLogger()) {
|
||||
super.log();
|
||||
getLogger()?.handleDerivedCreated(this);
|
||||
} else {
|
||||
super.log();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DerivedWithSetter<T, TChangeSummary = any> extends Derived<T, TChangeSummary> implements ISettableObservable<T> {
|
||||
constructor(
|
||||
debugNameData: DebugNameData,
|
||||
computeFn: (reader: IReader, changeSummary: TChangeSummary) => T,
|
||||
createChangeSummary: (() => TChangeSummary) | undefined,
|
||||
handleChange: ((context: IChangeContext, summary: TChangeSummary) => boolean) | undefined,
|
||||
handleLastObserverRemoved: (() => void) | undefined = undefined,
|
||||
equalityComparator: EqualityComparer<T>,
|
||||
public readonly set: (value: T, tx: ITransaction | undefined) => void,
|
||||
) {
|
||||
super(
|
||||
debugNameData,
|
||||
computeFn,
|
||||
createChangeSummary,
|
||||
handleChange,
|
||||
handleLastObserverRemoved,
|
||||
equalityComparator,
|
||||
);
|
||||
}
|
||||
}
|
||||
29
packages/core/src/observableInternal/index.ts
Normal file
29
packages/core/src/observableInternal/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This is a facade for the observable implementation. Only import from here!
|
||||
|
||||
export { observableValueOpts } from './api.js';
|
||||
export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges } from './autorun.js';
|
||||
export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js';
|
||||
export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './derived.js';
|
||||
export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js';
|
||||
export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js';
|
||||
export { constObservable, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js';
|
||||
export { type DebugOwner } from './debugName.js';
|
||||
|
||||
import {
|
||||
ConsoleObservableLogger,
|
||||
setLogger
|
||||
} from './logging.js';
|
||||
|
||||
// Remove "//" in the next line to enable logging
|
||||
const enableLogging = false
|
||||
// || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this
|
||||
;
|
||||
|
||||
if (enableLogging) {
|
||||
setLogger(new ConsoleObservableLogger());
|
||||
}
|
||||
148
packages/core/src/observableInternal/lazyObservableValue.ts
Normal file
148
packages/core/src/observableInternal/lazyObservableValue.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EqualityComparer } from './commonFacade/deps.js';
|
||||
import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js';
|
||||
import { DebugNameData } from './debugName.js';
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Holds off updating observers until the value is actually read.
|
||||
*/
|
||||
export class LazyObservableValue<T, TChange = void>
|
||||
extends BaseObservable<T, TChange>
|
||||
implements ISettableObservable<T, TChange> {
|
||||
protected _value: T;
|
||||
private _isUpToDate = true;
|
||||
private readonly _deltas: TChange[] = [];
|
||||
|
||||
get debugName() {
|
||||
return this._debugNameData.getDebugName(this) ?? 'LazyObservableValue';
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
initialValue: T,
|
||||
private readonly _equalityComparator: EqualityComparer<T>,
|
||||
) {
|
||||
super();
|
||||
this._value = initialValue;
|
||||
}
|
||||
|
||||
public override get(): T {
|
||||
this._update();
|
||||
return this._value;
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
if (this._isUpToDate) {
|
||||
return;
|
||||
}
|
||||
this._isUpToDate = true;
|
||||
|
||||
if (this._deltas.length > 0) {
|
||||
for (const change of this._deltas) {
|
||||
getLogger()?.handleObservableChanged(this, { change, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true });
|
||||
for (const observer of this.observers) {
|
||||
observer.handleChange(this, change);
|
||||
}
|
||||
}
|
||||
this._deltas.length = 0;
|
||||
} else {
|
||||
getLogger()?.handleObservableChanged(this, { change: undefined, didChange: true, oldValue: '(unknown)', newValue: this._value, hadValue: true });
|
||||
for (const observer of this.observers) {
|
||||
observer.handleChange(this, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateCounter = 0;
|
||||
|
||||
private _beginUpdate(): void {
|
||||
this._updateCounter++;
|
||||
if (this._updateCounter === 1) {
|
||||
for (const observer of this.observers) {
|
||||
observer.beginUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _endUpdate(): void {
|
||||
this._updateCounter--;
|
||||
if (this._updateCounter === 0) {
|
||||
this._update();
|
||||
|
||||
// End update could change the observer list.
|
||||
const observers = [...this.observers];
|
||||
for (const r of observers) {
|
||||
r.endUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override addObserver(observer: IObserver): void {
|
||||
const shouldCallBeginUpdate = !this.observers.has(observer) && this._updateCounter > 0;
|
||||
super.addObserver(observer);
|
||||
|
||||
if (shouldCallBeginUpdate) {
|
||||
observer.beginUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override removeObserver(observer: IObserver): void {
|
||||
const shouldCallEndUpdate = this.observers.has(observer) && this._updateCounter > 0;
|
||||
super.removeObserver(observer);
|
||||
|
||||
if (shouldCallEndUpdate) {
|
||||
// Calling end update after removing the observer makes sure endUpdate cannot be called twice here.
|
||||
observer.endUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
|
||||
if (change === undefined && this._equalityComparator(this._value, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _tx: TransactionImpl | undefined;
|
||||
if (!tx) {
|
||||
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
|
||||
}
|
||||
try {
|
||||
this._isUpToDate = false;
|
||||
this._setValue(value);
|
||||
if (change !== undefined) {
|
||||
this._deltas.push(change);
|
||||
}
|
||||
|
||||
tx.updateObserver({
|
||||
beginUpdate: () => this._beginUpdate(),
|
||||
endUpdate: () => this._endUpdate(),
|
||||
handleChange: (observable, change) => { },
|
||||
handlePossibleChange: (observable) => { },
|
||||
}, this);
|
||||
|
||||
if (this._updateCounter > 1) {
|
||||
// We already started begin/end update, so we need to manually call handlePossibleChange
|
||||
for (const observer of this.observers) {
|
||||
observer.handlePossibleChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (_tx) {
|
||||
_tx.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `${this.debugName}: ${this._value}`;
|
||||
}
|
||||
|
||||
protected _setValue(newValue: T): void {
|
||||
this._value = newValue;
|
||||
}
|
||||
}
|
||||
407
packages/core/src/observableInternal/logging.ts
Normal file
407
packages/core/src/observableInternal/logging.ts
Normal file
@ -0,0 +1,407 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AutorunObserver } from './autorun.js';
|
||||
import { IObservable, TransactionImpl } from './base.js';
|
||||
import { Derived } from './derived.js';
|
||||
import { FromEventObservable } from './utils.js';
|
||||
|
||||
let globalObservableLogger: IObservableLogger | undefined;
|
||||
|
||||
export function setLogger(logger: IObservableLogger): void {
|
||||
globalObservableLogger = logger;
|
||||
}
|
||||
|
||||
export function getLogger(): IObservableLogger | undefined {
|
||||
return globalObservableLogger;
|
||||
}
|
||||
|
||||
export function logObservable(obs: IObservable<any>): void {
|
||||
if (!globalObservableLogger) {
|
||||
const l = new ConsoleObservableLogger();
|
||||
l.addFilteredObj(obs);
|
||||
setLogger(l);
|
||||
} else {
|
||||
if (globalObservableLogger instanceof ConsoleObservableLogger) {
|
||||
(globalObservableLogger as ConsoleObservableLogger).addFilteredObj(obs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IChangeInformation {
|
||||
oldValue: unknown;
|
||||
newValue: unknown;
|
||||
change: unknown;
|
||||
didChange: boolean;
|
||||
hadValue: boolean;
|
||||
}
|
||||
|
||||
export interface IObservableLogger {
|
||||
handleObservableChanged(observable: IObservable<any>, info: IChangeInformation): void;
|
||||
handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void;
|
||||
|
||||
handleAutorunCreated(autorun: AutorunObserver): void;
|
||||
handleAutorunTriggered(autorun: AutorunObserver): void;
|
||||
handleAutorunFinished(autorun: AutorunObserver): void;
|
||||
|
||||
handleDerivedCreated(observable: Derived<any>): void;
|
||||
handleDerivedRecomputed(observable: Derived<any>, info: IChangeInformation): void;
|
||||
handleDerivedCleared(observable: Derived<any>): void;
|
||||
|
||||
handleBeginTransaction(transaction: TransactionImpl): void;
|
||||
handleEndTransaction(): void;
|
||||
}
|
||||
|
||||
export class ConsoleObservableLogger implements IObservableLogger {
|
||||
private indentation = 0;
|
||||
|
||||
private _filteredObjects: Set<unknown> | undefined;
|
||||
|
||||
public addFilteredObj(obj: unknown): void {
|
||||
if (!this._filteredObjects) {
|
||||
this._filteredObjects = new Set();
|
||||
}
|
||||
this._filteredObjects.add(obj);
|
||||
}
|
||||
|
||||
private _isIncluded(obj: unknown): boolean {
|
||||
return this._filteredObjects?.has(obj) ?? true;
|
||||
}
|
||||
|
||||
private textToConsoleArgs(text: ConsoleText): unknown[] {
|
||||
return consoleTextToArgs([
|
||||
normalText(repeat('| ', this.indentation)),
|
||||
text,
|
||||
]);
|
||||
}
|
||||
|
||||
private formatInfo(info: IChangeInformation): ConsoleText[] {
|
||||
if (!info.hadValue) {
|
||||
return [
|
||||
normalText(` `),
|
||||
styled(formatValue(info.newValue, 60), {
|
||||
color: 'green',
|
||||
}),
|
||||
normalText(` (initial)`),
|
||||
];
|
||||
}
|
||||
return info.didChange
|
||||
? [
|
||||
normalText(` `),
|
||||
styled(formatValue(info.oldValue, 70), {
|
||||
color: 'red',
|
||||
strikeThrough: true,
|
||||
}),
|
||||
normalText(` `),
|
||||
styled(formatValue(info.newValue, 60), {
|
||||
color: 'green',
|
||||
}),
|
||||
]
|
||||
: [normalText(` (unchanged)`)];
|
||||
}
|
||||
|
||||
handleObservableChanged(observable: IObservable<unknown>, info: IChangeInformation): void {
|
||||
if (!this._isIncluded(observable)) { return; }
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('observable value changed'),
|
||||
styled(observable.debugName, { color: 'BlueViolet' }),
|
||||
...this.formatInfo(info),
|
||||
]));
|
||||
}
|
||||
|
||||
private readonly changedObservablesSets = new WeakMap<object, Set<IObservable<any>>>();
|
||||
|
||||
formatChanges(changes: Set<IObservable<any>>): ConsoleText | undefined {
|
||||
if (changes.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return styled(
|
||||
' (changed deps: ' +
|
||||
[...changes].map((o) => o.debugName).join(', ') +
|
||||
')',
|
||||
{ color: 'gray' }
|
||||
);
|
||||
}
|
||||
|
||||
handleDerivedCreated(derived: Derived<unknown>): void {
|
||||
const existingHandleChange = derived.handleChange;
|
||||
this.changedObservablesSets.set(derived, new Set());
|
||||
derived.handleChange = (observable, change) => {
|
||||
this.changedObservablesSets.get(derived)!.add(observable);
|
||||
return existingHandleChange.apply(derived, [observable, change]);
|
||||
};
|
||||
|
||||
const debugTrackUpdating = false;
|
||||
if (debugTrackUpdating) {
|
||||
const updating: IObservable<any>[] = [];
|
||||
(derived as any).__debugUpdating = updating;
|
||||
|
||||
const existingBeginUpdate = derived.beginUpdate;
|
||||
derived.beginUpdate = (obs) => {
|
||||
updating.push(obs);
|
||||
return existingBeginUpdate.apply(derived, [obs]);
|
||||
};
|
||||
|
||||
const existingEndUpdate = derived.endUpdate;
|
||||
derived.endUpdate = (obs) => {
|
||||
const idx = updating.indexOf(obs);
|
||||
if (idx === -1) {
|
||||
console.error('endUpdate called without beginUpdate', derived.debugName, obs.debugName);
|
||||
}
|
||||
updating.splice(idx, 1);
|
||||
return existingEndUpdate.apply(derived, [obs]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
handleDerivedRecomputed(derived: Derived<unknown>, info: IChangeInformation): void {
|
||||
if (!this._isIncluded(derived)) { return; }
|
||||
|
||||
const changedObservables = this.changedObservablesSets.get(derived);
|
||||
if (!changedObservables) { return; }
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('derived recomputed'),
|
||||
styled(derived.debugName, { color: 'BlueViolet' }),
|
||||
...this.formatInfo(info),
|
||||
this.formatChanges(changedObservables),
|
||||
{ data: [{ fn: derived._debugNameData.referenceFn ?? derived._computeFn }] }
|
||||
]));
|
||||
changedObservables.clear();
|
||||
}
|
||||
|
||||
handleDerivedCleared(derived: Derived<unknown>): void {
|
||||
if (!this._isIncluded(derived)) { return; }
|
||||
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('derived cleared'),
|
||||
styled(derived.debugName, { color: 'BlueViolet' }),
|
||||
]));
|
||||
}
|
||||
|
||||
handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void {
|
||||
if (!this._isIncluded(observable)) { return; }
|
||||
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('observable from event triggered'),
|
||||
styled(observable.debugName, { color: 'BlueViolet' }),
|
||||
...this.formatInfo(info),
|
||||
{ data: [{ fn: observable._getValue }] }
|
||||
]));
|
||||
}
|
||||
|
||||
handleAutorunCreated(autorun: AutorunObserver): void {
|
||||
if (!this._isIncluded(autorun)) { return; }
|
||||
|
||||
const existingHandleChange = autorun.handleChange;
|
||||
this.changedObservablesSets.set(autorun, new Set());
|
||||
autorun.handleChange = (observable, change) => {
|
||||
this.changedObservablesSets.get(autorun)!.add(observable);
|
||||
return existingHandleChange.apply(autorun, [observable, change]);
|
||||
};
|
||||
}
|
||||
|
||||
handleAutorunTriggered(autorun: AutorunObserver): void {
|
||||
const changedObservables = this.changedObservablesSets.get(autorun);
|
||||
if (!changedObservables) { return; }
|
||||
|
||||
if (this._isIncluded(autorun)) {
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('autorun'),
|
||||
styled(autorun.debugName, { color: 'BlueViolet' }),
|
||||
this.formatChanges(changedObservables),
|
||||
{ data: [{ fn: autorun._debugNameData.referenceFn ?? autorun._runFn }] }
|
||||
]));
|
||||
}
|
||||
changedObservables.clear();
|
||||
this.indentation++;
|
||||
}
|
||||
|
||||
handleAutorunFinished(autorun: AutorunObserver): void {
|
||||
this.indentation--;
|
||||
}
|
||||
|
||||
handleBeginTransaction(transaction: TransactionImpl): void {
|
||||
let transactionName = transaction.getDebugName();
|
||||
if (transactionName === undefined) {
|
||||
transactionName = '';
|
||||
}
|
||||
if (this._isIncluded(transaction)) {
|
||||
console.log(...this.textToConsoleArgs([
|
||||
formatKind('transaction'),
|
||||
styled(transactionName, { color: 'BlueViolet' }),
|
||||
{ data: [{ fn: transaction._fn }] }
|
||||
]));
|
||||
}
|
||||
this.indentation++;
|
||||
}
|
||||
|
||||
handleEndTransaction(): void {
|
||||
this.indentation--;
|
||||
}
|
||||
}
|
||||
|
||||
type ConsoleText =
|
||||
| (ConsoleText | undefined)[]
|
||||
| { text: string; style: string; data?: unknown[] }
|
||||
| { data: unknown[] };
|
||||
|
||||
function consoleTextToArgs(text: ConsoleText): unknown[] {
|
||||
const styles = new Array<any>();
|
||||
const data: unknown[] = [];
|
||||
let firstArg = '';
|
||||
|
||||
function process(t: ConsoleText): void {
|
||||
if ('length' in t) {
|
||||
for (const item of t) {
|
||||
if (item) {
|
||||
process(item);
|
||||
}
|
||||
}
|
||||
} else if ('text' in t) {
|
||||
firstArg += `%c${t.text}`;
|
||||
styles.push(t.style);
|
||||
if (t.data) {
|
||||
data.push(...t.data);
|
||||
}
|
||||
} else if ('data' in t) {
|
||||
data.push(...t.data);
|
||||
}
|
||||
}
|
||||
|
||||
process(text);
|
||||
|
||||
const result = [firstArg, ...styles];
|
||||
result.push(...data);
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalText(text: string): ConsoleText {
|
||||
return styled(text, { color: 'black' });
|
||||
}
|
||||
|
||||
function formatKind(kind: string): ConsoleText {
|
||||
return styled(padStr(`${kind}: `, 10), { color: 'black', bold: true });
|
||||
}
|
||||
|
||||
function styled(
|
||||
text: string,
|
||||
options: { color: string; strikeThrough?: boolean; bold?: boolean } = {
|
||||
color: 'black',
|
||||
}
|
||||
): ConsoleText {
|
||||
function objToCss(styleObj: Record<string, string>): string {
|
||||
return Object.entries(styleObj).reduce(
|
||||
(styleString, [propName, propValue]) => {
|
||||
return `${styleString}${propName}:${propValue};`;
|
||||
},
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
const style: Record<string, string> = {
|
||||
color: options.color,
|
||||
};
|
||||
if (options.strikeThrough) {
|
||||
style['text-decoration'] = 'line-through';
|
||||
}
|
||||
if (options.bold) {
|
||||
style['font-weight'] = 'bold';
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
style: objToCss(style),
|
||||
};
|
||||
}
|
||||
|
||||
function formatValue(value: unknown, availableLen: number): string {
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
return '' + value;
|
||||
case 'string':
|
||||
if (value.length + 2 <= availableLen) {
|
||||
return `"${value}"`;
|
||||
}
|
||||
return `"${value.substr(0, availableLen - 7)}"+...`;
|
||||
|
||||
case 'boolean':
|
||||
return value ? 'true' : 'false';
|
||||
case 'undefined':
|
||||
return 'undefined';
|
||||
case 'object':
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return formatArray(value, availableLen);
|
||||
}
|
||||
return formatObject(value, availableLen);
|
||||
case 'symbol':
|
||||
return value.toString();
|
||||
case 'function':
|
||||
return `[[Function${value.name ? ' ' + value.name : ''}]]`;
|
||||
default:
|
||||
return '' + value;
|
||||
}
|
||||
}
|
||||
|
||||
function formatArray(value: unknown[], availableLen: number): string {
|
||||
let result = '[ ';
|
||||
let first = true;
|
||||
for (const val of value) {
|
||||
if (!first) {
|
||||
result += ', ';
|
||||
}
|
||||
if (result.length - 5 > availableLen) {
|
||||
result += '...';
|
||||
break;
|
||||
}
|
||||
first = false;
|
||||
result += `${formatValue(val, availableLen - result.length)}`;
|
||||
}
|
||||
result += ' ]';
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatObject(value: object, availableLen: number): string {
|
||||
if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) {
|
||||
const val = value.toString();
|
||||
if (val.length <= availableLen) {
|
||||
return val;
|
||||
}
|
||||
return val.substring(0, availableLen - 3) + '...';
|
||||
}
|
||||
|
||||
let result = '{ ';
|
||||
let first = true;
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
if (!first) {
|
||||
result += ', ';
|
||||
}
|
||||
if (result.length - 5 > availableLen) {
|
||||
result += '...';
|
||||
break;
|
||||
}
|
||||
first = false;
|
||||
result += `${key}: ${formatValue(val, availableLen - result.length)}`;
|
||||
}
|
||||
result += ' }';
|
||||
return result;
|
||||
}
|
||||
|
||||
function repeat(str: string, count: number): string {
|
||||
let result = '';
|
||||
for (let i = 1; i <= count; i++) {
|
||||
result += str;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function padStr(str: string, length: number): string {
|
||||
while (str.length < length) {
|
||||
str += ' ';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
117
packages/core/src/observableInternal/promise.ts
Normal file
117
packages/core/src/observableInternal/promise.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { IObservable, observableValue, transaction } from './base.js';
|
||||
import { derived } from './derived.js';
|
||||
|
||||
export class ObservableLazy<T> {
|
||||
private readonly _value = observableValue<T | undefined>(this, undefined);
|
||||
|
||||
/**
|
||||
* The cached value.
|
||||
* Does not force a computation of the value.
|
||||
*/
|
||||
public get cachedValue(): IObservable<T | undefined> { return this._value; }
|
||||
|
||||
constructor(private readonly _computeValue: () => T) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached value.
|
||||
* Computes the value if the value has not been cached yet.
|
||||
*/
|
||||
public getValue() {
|
||||
let v = this._value.get();
|
||||
if (!v) {
|
||||
v = this._computeValue();
|
||||
this._value.set(v, undefined);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise whose state is observable.
|
||||
*/
|
||||
export class ObservablePromise<T> {
|
||||
public static fromFn<T>(fn: () => Promise<T>): ObservablePromise<T> {
|
||||
return new ObservablePromise(fn());
|
||||
}
|
||||
|
||||
private readonly _value = observableValue<PromiseResult<T> | undefined>(this, undefined);
|
||||
|
||||
/**
|
||||
* The promise that this object wraps.
|
||||
*/
|
||||
public readonly promise: Promise<T>;
|
||||
|
||||
/**
|
||||
* The current state of the promise.
|
||||
* Is `undefined` if the promise didn't resolve yet.
|
||||
*/
|
||||
public readonly promiseResult: IObservable<PromiseResult<T> | undefined> = this._value;
|
||||
|
||||
constructor(promise: Promise<T>) {
|
||||
this.promise = promise.then(value => {
|
||||
transaction(tx => {
|
||||
/** @description onPromiseResolved */
|
||||
this._value.set(new PromiseResult(value, undefined), tx);
|
||||
});
|
||||
return value;
|
||||
}, error => {
|
||||
transaction(tx => {
|
||||
/** @description onPromiseRejected */
|
||||
this._value.set(new PromiseResult<T>(undefined, error), tx);
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PromiseResult<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The value of the resolved promise.
|
||||
* Undefined if the promise rejected.
|
||||
*/
|
||||
public readonly data: T | undefined,
|
||||
|
||||
/**
|
||||
* The error in case of a rejected promise.
|
||||
* Undefined if the promise resolved.
|
||||
*/
|
||||
public readonly error: unknown | undefined,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value if the promise resolved, otherwise throws the error.
|
||||
*/
|
||||
public getDataOrThrow(): T {
|
||||
if (this.error) {
|
||||
throw this.error;
|
||||
}
|
||||
return this.data!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A lazy promise whose state is observable.
|
||||
*/
|
||||
export class ObservableLazyPromise<T> {
|
||||
private readonly _lazyValue = new ObservableLazy(() => new ObservablePromise(this._computePromise()));
|
||||
|
||||
/**
|
||||
* Does not enforce evaluation of the promise compute function.
|
||||
* Is undefined if the promise has not been computed yet.
|
||||
*/
|
||||
public readonly cachedPromiseResult = derived(this, reader => this._lazyValue.cachedValue.read(reader)?.promiseResult.read(reader));
|
||||
|
||||
constructor(private readonly _computePromise: () => Promise<T>) {
|
||||
}
|
||||
|
||||
public getPromise(): Promise<T> {
|
||||
return this._lazyValue.getValue().promise;
|
||||
}
|
||||
}
|
||||
664
packages/core/src/observableInternal/utils.ts
Normal file
664
packages/core/src/observableInternal/utils.ts
Normal file
@ -0,0 +1,664 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js';
|
||||
import { BaseObservable, ConvenientObservable, IObservable, IObservableWithChange, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js';
|
||||
import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js';
|
||||
import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js';
|
||||
import { derived, derivedOpts } from './derived.js';
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Represents an efficient observable whose value never changes.
|
||||
*/
|
||||
export function constObservable<T>(value: T): IObservable<T> {
|
||||
return new ConstObservable(value);
|
||||
}
|
||||
|
||||
class ConstObservable<T> extends ConvenientObservable<T, void> {
|
||||
constructor(private readonly value: T) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override get debugName(): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
return this.value;
|
||||
}
|
||||
public addObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
public removeObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `Const: ${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
|
||||
const observable = observableValue<{ value?: T }>('promiseValue', {});
|
||||
promise.then((value) => {
|
||||
observable.set({ value }, undefined);
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
owner: DebugOwner,
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent(...args:
|
||||
[owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
| [event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
): IObservable<any> {
|
||||
let owner;
|
||||
let event;
|
||||
let getValue;
|
||||
if (args.length === 3) {
|
||||
[owner, event, getValue] = args;
|
||||
} else {
|
||||
[event, getValue] = args;
|
||||
}
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(owner, undefined, getValue),
|
||||
event,
|
||||
getValue,
|
||||
() => FromEventObservable.globalTransaction,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function observableFromEventOpts<T, TArgs = unknown>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
},
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T> {
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue),
|
||||
event,
|
||||
getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
||||
public static globalTransaction: ITransaction | undefined;
|
||||
|
||||
private value: T | undefined;
|
||||
private hasValue = false;
|
||||
private subscription: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
private readonly event: Event<TArgs>,
|
||||
public readonly _getValue: (args: TArgs | undefined) => T,
|
||||
private readonly _getTransaction: () => ITransaction | undefined,
|
||||
private readonly _equalityComparator: EqualityComparer<T>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getDebugName(): string | undefined {
|
||||
return this._debugNameData.getDebugName(this);
|
||||
}
|
||||
|
||||
public get debugName(): string {
|
||||
const name = this.getDebugName();
|
||||
return 'From Event' + (name ? `: ${name}` : '');
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this.subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = (args: TArgs | undefined) => {
|
||||
const newValue = this._getValue(args);
|
||||
const oldValue = this.value;
|
||||
|
||||
const didChange = !this.hasValue || !(this._equalityComparator(oldValue!, newValue));
|
||||
let didRunTransaction = false;
|
||||
|
||||
if (didChange) {
|
||||
this.value = newValue;
|
||||
|
||||
if (this.hasValue) {
|
||||
didRunTransaction = true;
|
||||
subtransaction(
|
||||
this._getTransaction(),
|
||||
(tx) => {
|
||||
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
|
||||
|
||||
for (const o of this.observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const name = this.getDebugName();
|
||||
return 'Event fired' + (name ? `: ${name}` : '');
|
||||
}
|
||||
);
|
||||
}
|
||||
this.hasValue = true;
|
||||
}
|
||||
|
||||
if (!didRunTransaction) {
|
||||
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
|
||||
}
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this.subscription!.dispose();
|
||||
this.subscription = undefined;
|
||||
this.hasValue = false;
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
if (this.subscription) {
|
||||
if (!this.hasValue) {
|
||||
this.handleEvent(undefined);
|
||||
}
|
||||
return this.value!;
|
||||
} else {
|
||||
// no cache, as there are no subscribers to keep it updated
|
||||
const value = this._getValue(undefined);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace observableFromEvent {
|
||||
export const Observer = FromEventObservable;
|
||||
|
||||
export function batchEventsGlobally(tx: ITransaction, fn: () => void): void {
|
||||
let didSet = false;
|
||||
if (FromEventObservable.globalTransaction === undefined) {
|
||||
FromEventObservable.globalTransaction = tx;
|
||||
didSet = true;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
if (didSet) {
|
||||
FromEventObservable.globalTransaction = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function observableSignalFromEvent(
|
||||
debugName: string,
|
||||
event: Event<any>
|
||||
): IObservable<void> {
|
||||
return new FromEventObservableSignal(debugName, event);
|
||||
}
|
||||
|
||||
class FromEventObservableSignal extends BaseObservable<void> {
|
||||
private subscription: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
public readonly debugName: string,
|
||||
private readonly event: Event<any>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this.subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = () => {
|
||||
transaction(
|
||||
(tx) => {
|
||||
for (const o of this.observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => this.debugName
|
||||
);
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this.subscription!.dispose();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signal that can be triggered to invalidate observers.
|
||||
* Signals don't have a value - when they are triggered they indicate a change.
|
||||
* However, signals can carry a delta that is passed to observers.
|
||||
*/
|
||||
export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(debugNameOrOwner: string | object): IObservableSignal<TDelta> {
|
||||
if (typeof debugNameOrOwner === 'string') {
|
||||
return new ObservableSignal<TDelta>(debugNameOrOwner);
|
||||
} else {
|
||||
return new ObservableSignal<TDelta>(undefined, debugNameOrOwner);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IObservableSignal<TChange> extends IObservableWithChange<void, TChange> {
|
||||
trigger(tx: ITransaction | undefined, change: TChange): void;
|
||||
}
|
||||
|
||||
class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
|
||||
public get debugName() {
|
||||
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return this.debugName;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugName: string | undefined,
|
||||
private readonly _owner?: object,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public trigger(tx: ITransaction | undefined, change: TChange): void {
|
||||
if (!tx) {
|
||||
transaction(tx => {
|
||||
this.trigger(tx, change);
|
||||
}, () => `Trigger signal ${this.debugName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const o of this.observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, change);
|
||||
}
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
export function signalFromObservable<T>(owner: DebugOwner | undefined, observable: IObservable<T>): IObservable<void> {
|
||||
return derivedOpts({
|
||||
owner,
|
||||
equalsFn: () => false,
|
||||
}, reader => {
|
||||
observable.read(reader);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `debouncedObservable2` instead.
|
||||
*/
|
||||
export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T | undefined> {
|
||||
const debouncedObservable = observableValue<T | undefined>('debounced', undefined);
|
||||
|
||||
let timeout: any = undefined;
|
||||
|
||||
disposableStore.add(autorun(reader => {
|
||||
/** @description debounce */
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
transaction(tx => {
|
||||
debouncedObservable.set(value, tx);
|
||||
});
|
||||
}, debounceMs);
|
||||
|
||||
}));
|
||||
|
||||
return debouncedObservable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that debounces the input observable.
|
||||
*/
|
||||
export function debouncedObservable2<T>(observable: IObservable<T>, debounceMs: number): IObservable<T> {
|
||||
let hasValue = false;
|
||||
let lastValue: T | undefined;
|
||||
|
||||
let timeout: any = undefined;
|
||||
|
||||
return observableFromEvent<T, void>(cb => {
|
||||
const d = autorun(reader => {
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (!hasValue) {
|
||||
hasValue = true;
|
||||
lastValue = value;
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
lastValue = value;
|
||||
cb();
|
||||
}, debounceMs);
|
||||
}
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
d.dispose();
|
||||
hasValue = false;
|
||||
lastValue = undefined;
|
||||
},
|
||||
};
|
||||
}, () => {
|
||||
if (hasValue) {
|
||||
return lastValue!;
|
||||
} else {
|
||||
return observable.get();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
|
||||
const observable = observableValue('triggeredRecently', false);
|
||||
|
||||
let timeout: any = undefined;
|
||||
|
||||
disposableStore.add(event(() => {
|
||||
observable.set(true, undefined);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
observable.set(false, undefined);
|
||||
}, timeoutMs);
|
||||
}));
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes sure the observable is being observed and keeps its cache alive.
|
||||
*/
|
||||
export function keepObserved<T>(observable: IObservable<T>): IDisposable {
|
||||
const o = new KeepAliveObserver(false, undefined);
|
||||
observable.addObserver(o);
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setKeepObserved(keepObserved);
|
||||
|
||||
/**
|
||||
* This converts the given observable into an autorun.
|
||||
*/
|
||||
export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>, handleValue?: (value: T) => void): IDisposable {
|
||||
const o = new KeepAliveObserver(true, handleValue);
|
||||
observable.addObserver(o);
|
||||
if (handleValue) {
|
||||
handleValue(observable.get());
|
||||
} else {
|
||||
observable.reportChanges();
|
||||
}
|
||||
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
|
||||
|
||||
export class KeepAliveObserver implements IObserver {
|
||||
private _counter = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _forceRecompute: boolean,
|
||||
private readonly _handleValue: ((value: any) => void) | undefined,
|
||||
) { }
|
||||
|
||||
beginUpdate<T>(observable: IObservable<T>): void {
|
||||
this._counter++;
|
||||
}
|
||||
|
||||
endUpdate<T>(observable: IObservable<T>): void {
|
||||
this._counter--;
|
||||
if (this._counter === 0 && this._forceRecompute) {
|
||||
if (this._handleValue) {
|
||||
this._handleValue(observable.get());
|
||||
} else {
|
||||
observable.reportChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlePossibleChange<T>(observable: IObservable<T>): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T>
|
||||
& { clearCache(transaction: ITransaction): void; setCache(newValue: T | undefined, tx: ITransaction | undefined): void } {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const onChange = observableSignal('derivedObservableWithWritableCache');
|
||||
const observable = derived(owner, reader => {
|
||||
onChange.read(reader);
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return Object.assign(observable, {
|
||||
clearCache: (tx: ITransaction) => {
|
||||
lastValue = undefined;
|
||||
onChange.trigger(tx);
|
||||
},
|
||||
setCache: (newValue: T | undefined, tx: ITransaction | undefined) => {
|
||||
lastValue = newValue;
|
||||
onChange.trigger(tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the items array changes, referential equal items are not mapped again.
|
||||
*/
|
||||
export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
|
||||
let m = new ArrayMap(map, keySelector);
|
||||
const self = derivedOpts({
|
||||
debugReferenceFn: map,
|
||||
owner,
|
||||
onLastObserverRemoved: () => {
|
||||
m.dispose();
|
||||
m = new ArrayMap(map);
|
||||
}
|
||||
}, (reader) => {
|
||||
m.setItems(items.read(reader));
|
||||
return m.getItems();
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
class ArrayMap<TIn, TOut, TKey> implements IDisposable {
|
||||
private readonly _cache = new Map<TKey, { out: TOut; store: DisposableStore }>();
|
||||
private _items: TOut[] = [];
|
||||
constructor(
|
||||
private readonly _map: (input: TIn, store: DisposableStore) => TOut,
|
||||
private readonly _keySelector?: (input: TIn) => TKey,
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._cache.forEach(entry => entry.store.dispose());
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
public setItems(items: readonly TIn[]): void {
|
||||
const newItems: TOut[] = [];
|
||||
const itemsToRemove = new Set(this._cache.keys());
|
||||
|
||||
for (const item of items) {
|
||||
const key = this._keySelector ? this._keySelector(item) : item as unknown as TKey;
|
||||
|
||||
let entry = this._cache.get(key);
|
||||
if (!entry) {
|
||||
const store = new DisposableStore();
|
||||
const out = this._map(item, store);
|
||||
entry = { out, store };
|
||||
this._cache.set(key, entry);
|
||||
} else {
|
||||
itemsToRemove.delete(key);
|
||||
}
|
||||
newItems.push(entry.out);
|
||||
}
|
||||
|
||||
for (const item of itemsToRemove) {
|
||||
const entry = this._cache.get(item)!;
|
||||
entry.store.dispose();
|
||||
this._cache.delete(item);
|
||||
}
|
||||
|
||||
this._items = newItems;
|
||||
}
|
||||
|
||||
public getItems(): TOut[] {
|
||||
return this._items;
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEvent<T> {
|
||||
constructor(public readonly observable: IObservable<T>) {
|
||||
}
|
||||
|
||||
get onDidChange(): Event<void> {
|
||||
return Event.fromObservableLight(this.observable);
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this.observable.get();
|
||||
}
|
||||
}
|
||||
|
||||
export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
|
||||
if (value instanceof ValueWithChangeEventFromObservable) {
|
||||
return value.observable;
|
||||
}
|
||||
return observableFromEvent(owner, value.onDidChange, () => value.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that has the latest changed value of the given observables.
|
||||
* Initially (and when not observed), it has the value of the last observable.
|
||||
* When observed and any of the observables change, it has the value of the last changed observable.
|
||||
* If multiple observables change in the same transaction, the last observable wins.
|
||||
*/
|
||||
export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
|
||||
if (observables.length === 0) {
|
||||
throw new BugIndicatingError();
|
||||
}
|
||||
|
||||
let hasLastChangedValue = false;
|
||||
let lastChangedValue: any = undefined;
|
||||
|
||||
const result = observableFromEvent<any, void>(owner, cb => {
|
||||
const store = new DisposableStore();
|
||||
for (const o of observables) {
|
||||
store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
|
||||
hasLastChangedValue = true;
|
||||
lastChangedValue = o.read(reader);
|
||||
cb();
|
||||
}));
|
||||
}
|
||||
store.add({
|
||||
dispose() {
|
||||
hasLastChangedValue = false;
|
||||
lastChangedValue = undefined;
|
||||
},
|
||||
});
|
||||
return store;
|
||||
}, () => {
|
||||
if (hasLastChangedValue) {
|
||||
return lastChangedValue;
|
||||
} else {
|
||||
return observables[observables.length - 1].get();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like a derived.
|
||||
* However, if the value is not undefined, it is cached and will not be recomputed anymore.
|
||||
* In that case, the derived will unsubscribe from its dependencies.
|
||||
*/
|
||||
export function derivedConstOnceDefined<T>(owner: DebugOwner, fn: (reader: IReader) => T): IObservable<T | undefined> {
|
||||
return derivedObservableWithCache<T | undefined>(owner, (reader, lastValue) => lastValue ?? fn(reader));
|
||||
}
|
||||
|
||||
type RemoveUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
export function runOnChange<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined<TChange>[]) => void): IDisposable {
|
||||
let _previousValue: T | undefined;
|
||||
return autorunWithStoreHandleChanges({
|
||||
createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined<TChange>[], didChange: false }),
|
||||
handleChange: (context, changeSummary) => {
|
||||
if (context.didChange(observable)) {
|
||||
const e = context.change;
|
||||
if (e !== undefined) {
|
||||
changeSummary.deltas.push(e as RemoveUndefined<TChange>);
|
||||
}
|
||||
changeSummary.didChange = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}, (reader, changeSummary) => {
|
||||
const value = observable.read(reader);
|
||||
const previousValue = _previousValue;
|
||||
if (changeSummary.didChange) {
|
||||
_previousValue = value;
|
||||
cb(value, previousValue, changeSummary.deltas);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function runOnChangeWithStore<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined<TChange>[], store: DisposableStore) => void): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = runOnChange(observable, (value, previousValue: undefined | T, deltas) => {
|
||||
store.clear();
|
||||
cb(value, previousValue, deltas, store);
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
disposable.dispose();
|
||||
store.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
98
packages/core/src/observableInternal/utilsCancellation.ts
Normal file
98
packages/core/src/observableInternal/utilsCancellation.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IReader, IObservable } from './base.js';
|
||||
import { DebugOwner, DebugNameData } from './debugName.js';
|
||||
import { CancellationError, CancellationToken, CancellationTokenSource } from './commonFacade/cancellation.js';
|
||||
import { Derived } from './derived.js';
|
||||
import { strictEquals } from './commonFacade/deps.js';
|
||||
import { autorun } from './autorun.js';
|
||||
|
||||
/**
|
||||
* Resolves the promise when the observables state matches the predicate.
|
||||
*/
|
||||
export function waitForState<T>(observable: IObservable<T | null | undefined>): Promise<T>;
|
||||
export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<TState>;
|
||||
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T>;
|
||||
export function waitForState<T>(observable: IObservable<T>, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise<T> {
|
||||
if (!predicate) {
|
||||
predicate = state => state !== null && state !== undefined;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let isImmediateRun = true;
|
||||
let shouldDispose = false;
|
||||
const stateObs = observable.map(state => {
|
||||
/** @description waitForState.state */
|
||||
return {
|
||||
isFinished: predicate(state),
|
||||
error: isError ? isError(state) : false,
|
||||
state
|
||||
};
|
||||
});
|
||||
const d = autorun(reader => {
|
||||
/** @description waitForState */
|
||||
const { isFinished, error, state } = stateObs.read(reader);
|
||||
if (isFinished || error) {
|
||||
if (isImmediateRun) {
|
||||
// The variable `d` is not initialized yet
|
||||
shouldDispose = true;
|
||||
} else {
|
||||
d.dispose();
|
||||
}
|
||||
if (error) {
|
||||
reject(error === true ? state : error);
|
||||
} else {
|
||||
resolve(state);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cancellationToken) {
|
||||
const dc = cancellationToken.onCancellationRequested(() => {
|
||||
d.dispose();
|
||||
dc.dispose();
|
||||
reject(new CancellationError());
|
||||
});
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
d.dispose();
|
||||
dc.dispose();
|
||||
reject(new CancellationError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
isImmediateRun = false;
|
||||
if (shouldDispose) {
|
||||
d.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function derivedWithCancellationToken<T>(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
|
||||
export function derivedWithCancellationToken<T>(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable<T>;
|
||||
export function derivedWithCancellationToken<T>(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: CancellationToken) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
let cancellationTokenSource: CancellationTokenSource | undefined = undefined;
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (cancellationTokenSource) {
|
||||
cancellationTokenSource.dispose(true);
|
||||
}
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
return computeFn(r, cancellationTokenSource.token);
|
||||
}, undefined,
|
||||
undefined,
|
||||
() => cancellationTokenSource?.dispose(),
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
1529
packages/core/src/path.ts
Normal file
1529
packages/core/src/path.ts
Normal file
File diff suppressed because it is too large
Load Diff
280
packages/core/src/platform.ts
Normal file
280
packages/core/src/platform.ts
Normal file
@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from './nls.js';
|
||||
|
||||
export const LANGUAGE_DEFAULT = 'en';
|
||||
|
||||
let _isWindows = false;
|
||||
let _isMacintosh = false;
|
||||
let _isLinux = false;
|
||||
let _isLinuxSnap = false;
|
||||
let _isNative = false;
|
||||
let _isWeb = false;
|
||||
let _isElectron = false;
|
||||
let _isIOS = false;
|
||||
let _isCI = false;
|
||||
let _isMobile = false;
|
||||
let _locale: string | undefined = undefined;
|
||||
let _language: string = LANGUAGE_DEFAULT;
|
||||
let _platformLocale: string = LANGUAGE_DEFAULT;
|
||||
let _translationsConfigFile: string | undefined = undefined;
|
||||
let _userAgent: string | undefined = undefined;
|
||||
|
||||
export interface IProcessEnvironment {
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is intentionally not identical to node.js
|
||||
* process because it also works in sandboxed environments
|
||||
* where the process object is implemented differently. We
|
||||
* define the properties here that we need for `platform`
|
||||
* to work and nothing else.
|
||||
*/
|
||||
export interface INodeProcess {
|
||||
platform: string;
|
||||
arch: string;
|
||||
env: IProcessEnvironment;
|
||||
versions?: {
|
||||
node?: string;
|
||||
electron?: string;
|
||||
chrome?: string;
|
||||
};
|
||||
type?: string;
|
||||
cwd: () => string;
|
||||
}
|
||||
|
||||
declare const process: INodeProcess;
|
||||
|
||||
const $globalThis: any = globalThis;
|
||||
|
||||
let nodeProcess: INodeProcess | undefined = undefined;
|
||||
if (typeof $globalThis.vscode !== 'undefined' && typeof $globalThis.vscode.process !== 'undefined') {
|
||||
// Native environment (sandboxed)
|
||||
nodeProcess = $globalThis.vscode.process;
|
||||
} else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') {
|
||||
// Native environment (non-sandboxed)
|
||||
nodeProcess = process;
|
||||
}
|
||||
|
||||
const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string';
|
||||
const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer';
|
||||
|
||||
interface INavigator {
|
||||
userAgent: string;
|
||||
maxTouchPoints?: number;
|
||||
language: string;
|
||||
}
|
||||
declare const navigator: INavigator;
|
||||
|
||||
// Native environment
|
||||
if (typeof nodeProcess === 'object') {
|
||||
_isWindows = (nodeProcess.platform === 'win32');
|
||||
_isMacintosh = (nodeProcess.platform === 'darwin');
|
||||
_isLinux = (nodeProcess.platform === 'linux');
|
||||
_isLinuxSnap = _isLinux && !!nodeProcess.env['SNAP'] && !!nodeProcess.env['SNAP_REVISION'];
|
||||
_isElectron = isElectronProcess;
|
||||
_isCI = !!nodeProcess.env['CI'] || !!nodeProcess.env['BUILD_ARTIFACTSTAGINGDIRECTORY'];
|
||||
_locale = LANGUAGE_DEFAULT;
|
||||
_language = LANGUAGE_DEFAULT;
|
||||
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
|
||||
if (rawNlsConfig) {
|
||||
try {
|
||||
const nlsConfig: nls.INLSConfiguration = JSON.parse(rawNlsConfig);
|
||||
_locale = nlsConfig.userLocale;
|
||||
_platformLocale = nlsConfig.osLocale;
|
||||
_language = nlsConfig.resolvedLanguage || LANGUAGE_DEFAULT;
|
||||
_translationsConfigFile = nlsConfig.languagePack?.translationsConfigFile;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
_isNative = true;
|
||||
}
|
||||
|
||||
// Web environment
|
||||
else if (typeof navigator === 'object' && !isElectronRenderer) {
|
||||
_userAgent = navigator.userAgent;
|
||||
_isWindows = _userAgent.indexOf('Windows') >= 0;
|
||||
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
|
||||
_isIOS = (_userAgent.indexOf('Macintosh') >= 0 || _userAgent.indexOf('iPad') >= 0 || _userAgent.indexOf('iPhone') >= 0) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
|
||||
_isLinux = _userAgent.indexOf('Linux') >= 0;
|
||||
_isMobile = _userAgent?.indexOf('Mobi') >= 0;
|
||||
_isWeb = true;
|
||||
_language = nls.getNLSLanguage() || LANGUAGE_DEFAULT;
|
||||
_locale = navigator.language.toLowerCase();
|
||||
_platformLocale = _locale;
|
||||
}
|
||||
|
||||
// Unknown environment
|
||||
else {
|
||||
console.error('Unable to resolve platform.');
|
||||
}
|
||||
|
||||
export const enum Platform {
|
||||
Web,
|
||||
Mac,
|
||||
Linux,
|
||||
Windows
|
||||
}
|
||||
export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux';
|
||||
|
||||
export function PlatformToString(platform: Platform): PlatformName {
|
||||
switch (platform) {
|
||||
case Platform.Web: return 'Web';
|
||||
case Platform.Mac: return 'Mac';
|
||||
case Platform.Linux: return 'Linux';
|
||||
case Platform.Windows: return 'Windows';
|
||||
}
|
||||
}
|
||||
|
||||
let _platform: Platform = Platform.Web;
|
||||
if (_isMacintosh) {
|
||||
_platform = Platform.Mac;
|
||||
} else if (_isWindows) {
|
||||
_platform = Platform.Windows;
|
||||
} else if (_isLinux) {
|
||||
_platform = Platform.Linux;
|
||||
}
|
||||
|
||||
export const isWindows = _isWindows;
|
||||
export const isMacintosh = _isMacintosh;
|
||||
export const isLinux = _isLinux;
|
||||
export const isLinuxSnap = _isLinuxSnap;
|
||||
export const isNative = _isNative;
|
||||
export const isElectron = _isElectron;
|
||||
export const isWeb = _isWeb;
|
||||
export const isWebWorker = (_isWeb && typeof $globalThis.importScripts === 'function');
|
||||
export const webWorkerOrigin = isWebWorker ? $globalThis.origin : undefined;
|
||||
export const isIOS = _isIOS;
|
||||
export const isMobile = _isMobile;
|
||||
/**
|
||||
* Whether we run inside a CI environment, such as
|
||||
* GH actions or Azure Pipelines.
|
||||
*/
|
||||
export const isCI = _isCI;
|
||||
export const platform = _platform;
|
||||
export const userAgent = _userAgent;
|
||||
|
||||
/**
|
||||
* The language used for the user interface. The format of
|
||||
* the string is all lower case (e.g. zh-tw for Traditional
|
||||
* Chinese or de for German)
|
||||
*/
|
||||
export const language = _language;
|
||||
|
||||
export namespace Language {
|
||||
|
||||
export function value(): string {
|
||||
return language;
|
||||
}
|
||||
|
||||
export function isDefaultVariant(): boolean {
|
||||
if (language.length === 2) {
|
||||
return language === 'en';
|
||||
} else if (language.length >= 3) {
|
||||
return language[0] === 'e' && language[1] === 'n' && language[2] === '-';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDefault(): boolean {
|
||||
return language === 'en';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop: The OS locale or the locale specified by --locale or `argv.json`.
|
||||
* Web: matches `platformLocale`.
|
||||
*
|
||||
* The UI is not necessarily shown in the provided locale.
|
||||
*/
|
||||
export const locale = _locale;
|
||||
|
||||
/**
|
||||
* This will always be set to the OS/browser's locale regardless of
|
||||
* what was specified otherwise. The format of the string is all
|
||||
* lower case (e.g. zh-tw for Traditional Chinese). The UI is not
|
||||
* necessarily shown in the provided locale.
|
||||
*/
|
||||
export const platformLocale = _platformLocale;
|
||||
|
||||
/**
|
||||
* The translations that are available through language packs.
|
||||
*/
|
||||
export const translationsConfigFile = _translationsConfigFile;
|
||||
|
||||
export const setTimeout0IsFaster = (typeof $globalThis.postMessage === 'function' && !$globalThis.importScripts);
|
||||
|
||||
/**
|
||||
* See https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#:~:text=than%204%2C%20then-,set%20timeout%20to%204,-.
|
||||
*
|
||||
* Works similarly to `setTimeout(0)` but doesn't suffer from the 4ms artificial delay
|
||||
* that browsers set when the nesting level is > 5.
|
||||
*/
|
||||
export const setTimeout0 = (() => {
|
||||
if (setTimeout0IsFaster) {
|
||||
interface IQueueElement {
|
||||
id: number;
|
||||
callback: () => void;
|
||||
}
|
||||
const pending: IQueueElement[] = [];
|
||||
|
||||
$globalThis.addEventListener('message', (e: any) => {
|
||||
if (e.data && e.data.vscodeScheduleAsyncWork) {
|
||||
for (let i = 0, len = pending.length; i < len; i++) {
|
||||
const candidate = pending[i];
|
||||
if (candidate.id === e.data.vscodeScheduleAsyncWork) {
|
||||
pending.splice(i, 1);
|
||||
candidate.callback();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let lastId = 0;
|
||||
return (callback: () => void) => {
|
||||
const myId = ++lastId;
|
||||
pending.push({
|
||||
id: myId,
|
||||
callback: callback
|
||||
});
|
||||
$globalThis.postMessage({ vscodeScheduleAsyncWork: myId }, '*');
|
||||
};
|
||||
}
|
||||
return (callback: () => void) => setTimeout(callback);
|
||||
})();
|
||||
|
||||
export const enum OperatingSystem {
|
||||
Windows = 1,
|
||||
Macintosh = 2,
|
||||
Linux = 3
|
||||
}
|
||||
export const OS = (_isMacintosh || _isIOS ? OperatingSystem.Macintosh : (_isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
|
||||
|
||||
let _isLittleEndian = true;
|
||||
let _isLittleEndianComputed = false;
|
||||
export function isLittleEndian(): boolean {
|
||||
if (!_isLittleEndianComputed) {
|
||||
_isLittleEndianComputed = true;
|
||||
const test = new Uint8Array(2);
|
||||
test[0] = 1;
|
||||
test[1] = 2;
|
||||
const view = new Uint16Array(test.buffer);
|
||||
_isLittleEndian = (view[0] === (2 << 8) + 1);
|
||||
}
|
||||
return _isLittleEndian;
|
||||
}
|
||||
|
||||
export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0);
|
||||
export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0);
|
||||
export const isSafari = !!(!isChrome && (userAgent && userAgent.indexOf('Safari') >= 0));
|
||||
export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0);
|
||||
export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0);
|
||||
|
||||
export function isBigSurOrNewer(osVersion: string): boolean {
|
||||
return parseFloat(osVersion) >= 20;
|
||||
}
|
||||
200
packages/core/src/primitives.ts
Normal file
200
packages/core/src/primitives.ts
Normal file
@ -0,0 +1,200 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
const _typeof = {
|
||||
number: 'number',
|
||||
string: 'string',
|
||||
undefined: 'undefined',
|
||||
object: 'object',
|
||||
function: 'function'
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type
|
||||
*/
|
||||
export function isTypedArray(obj: unknown): obj is Object {
|
||||
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
||||
return typeof obj === 'object'
|
||||
&& obj instanceof TypedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array or not.
|
||||
*/
|
||||
export function isArray(array: any): array is any[] {
|
||||
if (Array.isArray) {
|
||||
return Array.isArray(array);
|
||||
}
|
||||
|
||||
if (array && typeof (array.length) === _typeof.number && array.constructor === Array) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript String or not.
|
||||
*/
|
||||
export function isString(str: any): str is string {
|
||||
if (typeof (str) === _typeof.string || str instanceof String) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
|
||||
*/
|
||||
export function isStringArray(value: any): value is string[] {
|
||||
return isArray(value) && (value).every(elem => isString(elem));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns whether the provided parameter is of type `object` but **not**
|
||||
* `null`, an `array`, a `regexp`, nor a `date`.
|
||||
*/
|
||||
export function isObject(obj: any): boolean {
|
||||
// The method can't do a type cast since there are type (like strings) which
|
||||
// are subclasses of any put not positvely matched by the function. Hence type
|
||||
// narrowing results in wrong results.
|
||||
return typeof obj === _typeof.object
|
||||
&& obj !== null
|
||||
&& !Array.isArray(obj)
|
||||
&& !(obj instanceof RegExp)
|
||||
&& !(obj instanceof Date);
|
||||
}
|
||||
|
||||
/**
|
||||
* In **contrast** to just checking `typeof` this will return `false` for `NaN`.
|
||||
* @returns whether the provided parameter is a JavaScript Number or not.
|
||||
*/
|
||||
export function isNumber(obj: any): obj is number {
|
||||
if ((typeof (obj) === _typeof.number || obj instanceof Number) && !isNaN(obj)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Boolean or not.
|
||||
*/
|
||||
export function isBoolean(obj: any): obj is boolean {
|
||||
return obj === true || obj === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined.
|
||||
*/
|
||||
export function isUndefined(obj: any): boolean {
|
||||
return typeof (obj) === _typeof.undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined or null.
|
||||
*/
|
||||
export function isUndefinedOrNull(obj: any): boolean {
|
||||
return isUndefined(obj) || obj === null;
|
||||
}
|
||||
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is an empty JavaScript Object or not.
|
||||
*/
|
||||
export function isEmptyObject(obj: any): obj is any {
|
||||
if (!isObject(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (hasOwnProperty.call(obj, key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Function or not.
|
||||
*/
|
||||
export function isFunction(obj: any): obj is Function {
|
||||
return typeof obj === _typeof.function;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameters is are JavaScript Function or not.
|
||||
*/
|
||||
export function areFunctions(...objects: any[]): boolean {
|
||||
return objects && objects.length > 0 && objects.every(isFunction);
|
||||
}
|
||||
|
||||
export type TypeConstraint = string | Function;
|
||||
|
||||
export function validateConstraints(args: any[], constraints: TypeConstraint[]): void {
|
||||
const len = Math.min(args.length, constraints.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
validateConstraint(args[i], constraints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateConstraint(arg: any, constraint: TypeConstraint): void {
|
||||
|
||||
if (isString(constraint)) {
|
||||
if (typeof arg !== constraint) {
|
||||
throw new Error(`argument does not match constraint: typeof ${constraint}`);
|
||||
}
|
||||
} else if (isFunction(constraint)) {
|
||||
if (arg instanceof constraint) {
|
||||
return;
|
||||
}
|
||||
if (arg && arg.constructor === constraint) {
|
||||
return;
|
||||
}
|
||||
if (constraint.length === 1 && constraint.call(undefined, arg) === true) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object of the provided class and will call the constructor with
|
||||
* any additional argument supplied.
|
||||
*/
|
||||
export function create(ctor: Function, ...args: any[]): any {
|
||||
const obj = Object.create(ctor.prototype);
|
||||
ctor.apply(obj, args);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export type IFunction0<T> = () => T;
|
||||
export type IFunction1<A1, T> = (a1: A1) => T;
|
||||
export type IFunction2<A1, A2, T> = (a1: A1, a2: A2) => T;
|
||||
export type IFunction3<A1, A2, A3, T> = (a1: A1, a2: A2, a3: A3) => T;
|
||||
export type IFunction4<A1, A2, A3, A4, T> = (a1: A1, a2: A2, a3: A3, a4: A4) => T;
|
||||
export type IFunction5<A1, A2, A3, A4, A5, T> = (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => T;
|
||||
export type IFunction6<A1, A2, A3, A4, A5, A6, T> = (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => T;
|
||||
export type IFunction7<A1, A2, A3, A4, A5, A6, A7, T> = (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => T;
|
||||
export type IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, T> = (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8) => T;
|
||||
|
||||
export interface IAction0 extends IFunction0<void> { }
|
||||
export interface IAction1<A1> extends IFunction1<A1, void> { }
|
||||
export interface IAction2<A1, A2> extends IFunction2<A1, A2, void> { }
|
||||
export interface IAction3<A1, A2, A3> extends IFunction3<A1, A2, A3, void> { }
|
||||
export interface IAction4<A1, A2, A3, A4> extends IFunction4<A1, A2, A3, A4, void> { }
|
||||
export interface IAction5<A1, A2, A3, A4, A5> extends IFunction5<A1, A2, A3, A4, A5, void> { }
|
||||
export interface IAction6<A1, A2, A3, A4, A5, A6> extends IFunction6<A1, A2, A3, A4, A5, A6, void> { }
|
||||
export interface IAction7<A1, A2, A3, A4, A5, A6, A7> extends IFunction7<A1, A2, A3, A4, A5, A6, A7, void> { }
|
||||
export interface IAction8<A1, A2, A3, A4, A5, A6, A7, A8> extends IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, void> { }
|
||||
|
||||
export type NumberCallback = (index: number) => void;
|
||||
76
packages/core/src/process.ts
Normal file
76
packages/core/src/process.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isMacintosh, isWindows, type INodeProcess } from './platform.js';
|
||||
|
||||
let safeProcess: Omit<INodeProcess, 'arch'> & { arch: string | undefined };
|
||||
declare const process: INodeProcess;
|
||||
|
||||
// Native sandbox environment
|
||||
const vscodeGlobal = (globalThis as any).vscode;
|
||||
if (typeof vscodeGlobal !== 'undefined' && typeof vscodeGlobal.process !== 'undefined') {
|
||||
const sandboxProcess: INodeProcess = vscodeGlobal.process;
|
||||
safeProcess = {
|
||||
get platform() { return sandboxProcess.platform; },
|
||||
get arch() { return sandboxProcess.arch; },
|
||||
get env() { return sandboxProcess.env; },
|
||||
cwd() { return sandboxProcess.cwd(); }
|
||||
};
|
||||
}
|
||||
|
||||
// Native node.js environment
|
||||
else if (typeof process !== 'undefined' && typeof process?.versions?.node === 'string') {
|
||||
safeProcess = {
|
||||
get platform() { return process.platform; },
|
||||
get arch() { return process.arch; },
|
||||
get env() { return process.env; },
|
||||
cwd() { return process.env['VSCODE_CWD'] || process.cwd(); }
|
||||
};
|
||||
}
|
||||
|
||||
// Web environment
|
||||
else {
|
||||
safeProcess = {
|
||||
|
||||
// Supported
|
||||
get platform() { return isWindows ? 'win32' : isMacintosh ? 'darwin' : 'linux'; },
|
||||
get arch() { return undefined; /* arch is undefined in web */ },
|
||||
|
||||
// Unsupported
|
||||
get env() { return {}; },
|
||||
cwd() { return '/'; }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides safe access to the `cwd` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*
|
||||
* Note: in web, this property is hardcoded to be `/`.
|
||||
*
|
||||
* @skipMangle
|
||||
*/
|
||||
export const cwd = safeProcess.cwd;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `env` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*
|
||||
* Note: in web, this property is hardcoded to be `{}`.
|
||||
*/
|
||||
export const env = safeProcess.env;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `platform` property in node.js, sandboxed or web
|
||||
* environments.
|
||||
*/
|
||||
export const platform = safeProcess.platform;
|
||||
|
||||
/**
|
||||
* Provides safe access to the `arch` method in node.js, sandboxed or web
|
||||
* environments.
|
||||
* Note: `arch` is `undefined` in web
|
||||
*/
|
||||
export const arch = safeProcess.arch;
|
||||
444
packages/core/src/resources.ts
Normal file
444
packages/core/src/resources.ts
Normal file
@ -0,0 +1,444 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from './charCode.js';
|
||||
import * as extpath from './extpath.js';
|
||||
import { Schemas } from './network.js';
|
||||
import * as paths from './path.js';
|
||||
import { isLinux, isWindows } from './platform.js';
|
||||
import { compare as strCompare, equalsIgnoreCase } from './strings.js';
|
||||
import { URI, uriToFsPath } from './uri.js';
|
||||
|
||||
export function originalFSPath(uri: URI): string {
|
||||
return uriToFsPath(uri, true);
|
||||
}
|
||||
|
||||
//#region IExtUri
|
||||
|
||||
export interface IExtUri {
|
||||
|
||||
// --- identity
|
||||
|
||||
/**
|
||||
* Compares two uris.
|
||||
*
|
||||
* @param uri1 Uri
|
||||
* @param uri2 Uri
|
||||
* @param ignoreFragment Ignore the fragment (defaults to `false`)
|
||||
*/
|
||||
compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;
|
||||
|
||||
/**
|
||||
* Tests whether two uris are equal
|
||||
*
|
||||
* @param uri1 Uri
|
||||
* @param uri2 Uri
|
||||
* @param ignoreFragment Ignore the fragment (defaults to `false`)
|
||||
*/
|
||||
isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
|
||||
*
|
||||
* @param base A uri which is "longer" or at least same length as `parentCandidate`
|
||||
* @param parentCandidate A uri which is "shorter" or up to same length as `base`
|
||||
* @param ignoreFragment Ignore the fragment (defaults to `false`)
|
||||
*/
|
||||
isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
|
||||
* @see {@link ResourceMap}
|
||||
* @param uri Uri
|
||||
* @param ignoreFragment Ignore the fragment (defaults to `false`)
|
||||
*/
|
||||
getComparisonKey(uri: URI, ignoreFragment?: boolean): string;
|
||||
|
||||
/**
|
||||
* Whether the casing of the path-component of the uri should be ignored.
|
||||
*/
|
||||
ignorePathCasing(uri: URI): boolean;
|
||||
|
||||
// --- path math
|
||||
|
||||
basenameOrAuthority(resource: URI): string;
|
||||
|
||||
/**
|
||||
* Returns the basename of the path component of an uri.
|
||||
* @param resource
|
||||
*/
|
||||
basename(resource: URI): string;
|
||||
|
||||
/**
|
||||
* Returns the extension of the path component of an uri.
|
||||
* @param resource
|
||||
*/
|
||||
extname(resource: URI): string;
|
||||
/**
|
||||
* Return a URI representing the directory of a URI path.
|
||||
*
|
||||
* @param resource The input URI.
|
||||
* @returns The URI representing the directory of the input URI.
|
||||
*/
|
||||
dirname(resource: URI): URI;
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param resource The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
joinPath(resource: URI, ...pathFragment: string[]): URI;
|
||||
/**
|
||||
* Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names.
|
||||
*
|
||||
* @param resource The URI to normalize the path.
|
||||
* @returns The URI with the normalized path.
|
||||
*/
|
||||
normalizePath(resource: URI): URI;
|
||||
/**
|
||||
*
|
||||
* @param from
|
||||
* @param to
|
||||
*/
|
||||
relativePath(from: URI, to: URI): string | undefined;
|
||||
/**
|
||||
* Resolves an absolute or relative path against a base URI.
|
||||
* The path can be relative or absolute posix or a Windows path
|
||||
*/
|
||||
resolvePath(base: URI, path: string): URI;
|
||||
|
||||
// --- misc
|
||||
|
||||
/**
|
||||
* Returns true if the URI path is absolute.
|
||||
*/
|
||||
isAbsolutePath(resource: URI): boolean;
|
||||
/**
|
||||
* Tests whether the two authorities are the same
|
||||
*/
|
||||
isEqualAuthority(a1: string, a2: string): boolean;
|
||||
/**
|
||||
* Returns true if the URI path has a trailing path separator
|
||||
*/
|
||||
hasTrailingPathSeparator(resource: URI, sep?: string): boolean;
|
||||
/**
|
||||
* Removes a trailing path separator, if there's one.
|
||||
* Important: Doesn't remove the first slash, it would make the URI invalid
|
||||
*/
|
||||
removeTrailingPathSeparator(resource: URI, sep?: string): URI;
|
||||
/**
|
||||
* Adds a trailing path separator to the URI if there isn't one already.
|
||||
* For example, c:\ would be unchanged, but c:\users would become c:\users\
|
||||
*/
|
||||
addTrailingPathSeparator(resource: URI, sep?: string): URI;
|
||||
}
|
||||
|
||||
export class ExtUri implements IExtUri {
|
||||
|
||||
constructor(private _ignorePathCasing: (uri: URI) => boolean) { }
|
||||
|
||||
compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
|
||||
if (uri1 === uri2) {
|
||||
return 0;
|
||||
}
|
||||
return strCompare(this.getComparisonKey(uri1, ignoreFragment), this.getComparisonKey(uri2, ignoreFragment));
|
||||
}
|
||||
|
||||
isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean {
|
||||
if (uri1 === uri2) {
|
||||
return true;
|
||||
}
|
||||
if (!uri1 || !uri2) {
|
||||
return false;
|
||||
}
|
||||
return this.getComparisonKey(uri1, ignoreFragment) === this.getComparisonKey(uri2, ignoreFragment);
|
||||
}
|
||||
|
||||
getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
|
||||
return uri.with({
|
||||
path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined,
|
||||
fragment: ignoreFragment ? null : undefined
|
||||
}).toString();
|
||||
}
|
||||
|
||||
ignorePathCasing(uri: URI): boolean {
|
||||
return this._ignorePathCasing(uri);
|
||||
}
|
||||
|
||||
isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment: boolean = false): boolean {
|
||||
if (base.scheme === parentCandidate.scheme) {
|
||||
if (base.scheme === Schemas.file) {
|
||||
return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), this._ignorePathCasing(base)) && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment);
|
||||
}
|
||||
if (isEqualAuthority(base.authority, parentCandidate.authority)) {
|
||||
return extpath.isEqualOrParent(base.path, parentCandidate.path, this._ignorePathCasing(base), '/') && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- path math
|
||||
|
||||
joinPath(resource: URI, ...pathFragment: string[]): URI {
|
||||
return URI.joinPath(resource, ...pathFragment);
|
||||
}
|
||||
|
||||
basenameOrAuthority(resource: URI): string {
|
||||
return basename(resource) || resource.authority;
|
||||
}
|
||||
|
||||
basename(resource: URI): string {
|
||||
return paths.posix.basename(resource.path);
|
||||
}
|
||||
|
||||
extname(resource: URI): string {
|
||||
return paths.posix.extname(resource.path);
|
||||
}
|
||||
|
||||
dirname(resource: URI): URI {
|
||||
if (resource.path.length === 0) {
|
||||
return resource;
|
||||
}
|
||||
let dirname;
|
||||
if (resource.scheme === Schemas.file) {
|
||||
dirname = URI.file(paths.dirname(originalFSPath(resource))).path;
|
||||
} else {
|
||||
dirname = paths.posix.dirname(resource.path);
|
||||
if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) {
|
||||
console.error(`dirname("${resource.toString})) resulted in a relative path`);
|
||||
dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character
|
||||
}
|
||||
}
|
||||
return resource.with({
|
||||
path: dirname
|
||||
});
|
||||
}
|
||||
|
||||
normalizePath(resource: URI): URI {
|
||||
if (!resource.path.length) {
|
||||
return resource;
|
||||
}
|
||||
let normalizedPath: string;
|
||||
if (resource.scheme === Schemas.file) {
|
||||
normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path;
|
||||
} else {
|
||||
normalizedPath = paths.posix.normalize(resource.path);
|
||||
}
|
||||
return resource.with({
|
||||
path: normalizedPath
|
||||
});
|
||||
}
|
||||
|
||||
relativePath(from: URI, to: URI): string | undefined {
|
||||
if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
|
||||
return undefined;
|
||||
}
|
||||
if (from.scheme === Schemas.file) {
|
||||
const relativePath = paths.relative(originalFSPath(from), originalFSPath(to));
|
||||
return isWindows ? extpath.toSlashes(relativePath) : relativePath;
|
||||
}
|
||||
let fromPath = from.path || '/';
|
||||
const toPath = to.path || '/';
|
||||
if (this._ignorePathCasing(from)) {
|
||||
// make casing of fromPath match toPath
|
||||
let i = 0;
|
||||
for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) {
|
||||
if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) {
|
||||
if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fromPath = toPath.substr(0, i) + fromPath.substr(i);
|
||||
}
|
||||
return paths.posix.relative(fromPath, toPath);
|
||||
}
|
||||
|
||||
resolvePath(base: URI, path: string): URI {
|
||||
if (base.scheme === Schemas.file) {
|
||||
const newURI = URI.file(paths.resolve(originalFSPath(base), path));
|
||||
return base.with({
|
||||
authority: newURI.authority,
|
||||
path: newURI.path
|
||||
});
|
||||
}
|
||||
path = extpath.toPosixPath(path); // we allow path to be a windows path
|
||||
return base.with({
|
||||
path: paths.posix.resolve(base.path, path)
|
||||
});
|
||||
}
|
||||
|
||||
// --- misc
|
||||
|
||||
isAbsolutePath(resource: URI): boolean {
|
||||
return !!resource.path && resource.path[0] === '/';
|
||||
}
|
||||
|
||||
isEqualAuthority(a1: string | undefined, a2: string | undefined) {
|
||||
return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2));
|
||||
}
|
||||
|
||||
hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
const fsp = originalFSPath(resource);
|
||||
return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep;
|
||||
} else {
|
||||
const p = resource.path;
|
||||
return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0
|
||||
}
|
||||
}
|
||||
|
||||
removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
|
||||
// Make sure that the path isn't a drive letter. A trailing separator there is not removable.
|
||||
if (hasTrailingPathSeparator(resource, sep)) {
|
||||
return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI {
|
||||
let isRootSep: boolean = false;
|
||||
if (resource.scheme === Schemas.file) {
|
||||
const fsp = originalFSPath(resource);
|
||||
isRootSep = ((fsp !== undefined) && (fsp.length === extpath.getRoot(fsp).length) && (fsp[fsp.length - 1] === sep));
|
||||
} else {
|
||||
sep = '/';
|
||||
const p = resource.path;
|
||||
isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash;
|
||||
}
|
||||
if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) {
|
||||
return resource.with({ path: resource.path + '/' });
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unbiased utility that takes uris "as they are". This means it can be interchanged with
|
||||
* uri#toString() usages. The following is true
|
||||
* ```
|
||||
* assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
|
||||
* ```
|
||||
*/
|
||||
export const extUri = new ExtUri(() => false);
|
||||
|
||||
/**
|
||||
* BIASED utility that _mostly_ ignored the case of urs paths. ONLY use this util if you
|
||||
* understand what you are doing.
|
||||
*
|
||||
* This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
|
||||
*
|
||||
* When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
|
||||
* because those uris come from a "trustworthy source". When creating unknown uris it's always
|
||||
* better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
|
||||
* casing matters.
|
||||
*/
|
||||
export const extUriBiasedIgnorePathCase = new ExtUri(uri => {
|
||||
// A file scheme resource is in the same platform as code, so ignore case for non linux platforms
|
||||
// Resource can be from another platform. Lowering the case as an hack. Should come from File system provider
|
||||
return uri.scheme === Schemas.file ? !isLinux : true;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* BIASED utility that always ignores the casing of uris paths. ONLY use this util if you
|
||||
* understand what you are doing.
|
||||
*
|
||||
* This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged.
|
||||
*
|
||||
* When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient
|
||||
* because those uris come from a "trustworthy source". When creating unknown uris it's always
|
||||
* better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path
|
||||
* casing matters.
|
||||
*/
|
||||
export const extUriIgnorePathCase = new ExtUri(_ => true);
|
||||
|
||||
export const isEqual = extUri.isEqual.bind(extUri);
|
||||
export const isEqualOrParent = extUri.isEqualOrParent.bind(extUri);
|
||||
export const getComparisonKey = extUri.getComparisonKey.bind(extUri);
|
||||
export const basenameOrAuthority = extUri.basenameOrAuthority.bind(extUri);
|
||||
export const basename = extUri.basename.bind(extUri);
|
||||
export const extname = extUri.extname.bind(extUri);
|
||||
export const dirname = extUri.dirname.bind(extUri);
|
||||
export const joinPath = extUri.joinPath.bind(extUri);
|
||||
export const normalizePath = extUri.normalizePath.bind(extUri);
|
||||
export const relativePath = extUri.relativePath.bind(extUri);
|
||||
export const resolvePath = extUri.resolvePath.bind(extUri);
|
||||
export const isAbsolutePath = extUri.isAbsolutePath.bind(extUri);
|
||||
export const isEqualAuthority = extUri.isEqualAuthority.bind(extUri);
|
||||
export const hasTrailingPathSeparator = extUri.hasTrailingPathSeparator.bind(extUri);
|
||||
export const removeTrailingPathSeparator = extUri.removeTrailingPathSeparator.bind(extUri);
|
||||
export const addTrailingPathSeparator = extUri.addTrailingPathSeparator.bind(extUri);
|
||||
|
||||
//#endregion
|
||||
|
||||
export function distinctParents<T>(items: T[], resourceAccessor: (item: T) => URI): T[] {
|
||||
const distinctParents: T[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const candidateResource = resourceAccessor(items[i]);
|
||||
if (items.some((otherItem, index) => {
|
||||
if (index === i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isEqualOrParent(candidateResource, resourceAccessor(otherItem));
|
||||
})) {
|
||||
continue;
|
||||
}
|
||||
|
||||
distinctParents.push(items[i]);
|
||||
}
|
||||
|
||||
return distinctParents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data URI related helpers.
|
||||
*/
|
||||
export namespace DataUri {
|
||||
|
||||
export const META_DATA_LABEL = 'label';
|
||||
export const META_DATA_DESCRIPTION = 'description';
|
||||
export const META_DATA_SIZE = 'size';
|
||||
export const META_DATA_MIME = 'mime';
|
||||
|
||||
export function parseMetaData(dataUri: URI): Map<string, string> {
|
||||
const metadata = new Map<string, string>();
|
||||
|
||||
// Given a URI of: data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5...
|
||||
// the metadata is: size:2313;label:SomeLabel;description:SomeDescription
|
||||
const meta = dataUri.path.substring(dataUri.path.indexOf(';') + 1, dataUri.path.lastIndexOf(';'));
|
||||
meta.split(';').forEach(property => {
|
||||
const [key, value] = property.split(':');
|
||||
if (key && value) {
|
||||
metadata.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Given a URI of: data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5...
|
||||
// the mime is: image/png
|
||||
const mime = dataUri.path.substring(0, dataUri.path.indexOf(';'));
|
||||
if (mime) {
|
||||
metadata.set(META_DATA_MIME, mime);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
export function toLocalResource(resource: URI, authority: string | undefined, localScheme: string): URI {
|
||||
if (authority) {
|
||||
let path = resource.path;
|
||||
if (path && path[0] !== paths.posix.sep) {
|
||||
path = paths.posix.sep + path;
|
||||
}
|
||||
|
||||
return resource.with({ scheme: localScheme, authority, path });
|
||||
}
|
||||
|
||||
return resource.with({ scheme: localScheme });
|
||||
}
|
||||
34
packages/core/src/sequence.ts
Normal file
34
packages/core/src/sequence.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from './event.js';
|
||||
|
||||
export interface ISplice<T> {
|
||||
readonly start: number;
|
||||
readonly deleteCount: number;
|
||||
readonly toInsert: readonly T[];
|
||||
}
|
||||
|
||||
export interface ISpliceable<T> {
|
||||
splice(start: number, deleteCount: number, toInsert: readonly T[]): void;
|
||||
}
|
||||
|
||||
export interface ISequence<T> {
|
||||
readonly elements: T[];
|
||||
readonly onDidSplice: Event<ISplice<T>>;
|
||||
}
|
||||
|
||||
export class Sequence<T> implements ISequence<T>, ISpliceable<T> {
|
||||
|
||||
readonly elements: T[] = [];
|
||||
|
||||
private readonly _onDidSplice = new Emitter<ISplice<T>>();
|
||||
readonly onDidSplice: Event<ISplice<T>> = this._onDidSplice.event;
|
||||
|
||||
splice(start: number, deleteCount: number, toInsert: readonly T[] = []): void {
|
||||
this.elements.splice(start, deleteCount, ...toInsert);
|
||||
this._onDidSplice.fire({ start, deleteCount, toInsert });
|
||||
}
|
||||
}
|
||||
38
packages/core/src/set.ts
Normal file
38
packages/core/src/set.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class ArraySet<T> {
|
||||
|
||||
private _elements: T[];
|
||||
|
||||
constructor(elements: T[] = []) {
|
||||
this._elements = elements.slice();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._elements.length;
|
||||
}
|
||||
|
||||
set(element: T): void {
|
||||
this.unset(element);
|
||||
this._elements.push(element);
|
||||
}
|
||||
|
||||
contains(element: T): boolean {
|
||||
return this._elements.includes(element);
|
||||
}
|
||||
|
||||
unset(element: T): void {
|
||||
const index = this._elements.indexOf(element);
|
||||
|
||||
if (index > -1) {
|
||||
this._elements.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
get elements(): T[] {
|
||||
return this._elements.slice();
|
||||
}
|
||||
}
|
||||
43
packages/core/src/stopwatch.ts
Normal file
43
packages/core/src/stopwatch.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// fake definition so that the valid layers check won't trip on this
|
||||
declare const globalThis: { performance?: { now(): number } };
|
||||
|
||||
const hasPerformanceNow = (globalThis.performance && typeof globalThis.performance.now === 'function');
|
||||
|
||||
export class StopWatch {
|
||||
|
||||
private _startTime: number;
|
||||
private _stopTime: number;
|
||||
|
||||
private readonly _now: () => number;
|
||||
|
||||
public static create(highResolution?: boolean): StopWatch {
|
||||
return new StopWatch(highResolution);
|
||||
}
|
||||
|
||||
constructor(highResolution?: boolean) {
|
||||
this._now = hasPerformanceNow && highResolution === false ? Date.now : globalThis.performance!.now.bind(globalThis.performance);
|
||||
this._startTime = this._now();
|
||||
this._stopTime = -1;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this._stopTime = this._now();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._startTime = this._now();
|
||||
this._stopTime = -1;
|
||||
}
|
||||
|
||||
public elapsed(): number {
|
||||
if (this._stopTime !== -1) {
|
||||
return this._stopTime - this._startTime;
|
||||
}
|
||||
return this._now() - this._startTime;
|
||||
}
|
||||
}
|
||||
1334
packages/core/src/strings.ts
Normal file
1334
packages/core/src/strings.ts
Normal file
File diff suppressed because one or more lines are too long
9
packages/core/src/symbols.ts
Normal file
9
packages/core/src/symbols.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Can be passed into the Delayed to defer using a microtask
|
||||
* */
|
||||
export const MicrotaskDelay = Symbol('MicrotaskDelay');
|
||||
299
packages/core/src/types.ts
Normal file
299
packages/core/src/types.ts
Normal file
@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { assert } from './assert.js';
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript String or not.
|
||||
*/
|
||||
export function isString(str: unknown): str is string {
|
||||
return (typeof str === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
|
||||
*/
|
||||
export function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && (<unknown[]>value).every(elem => isString(elem));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is of type `object` but **not**
|
||||
* `null`, an `array`, a `regexp`, nor a `date`.
|
||||
*/
|
||||
export function isObject(obj: unknown): obj is Object {
|
||||
// The method can't do a type cast since there are type (like strings) which
|
||||
// are subclasses of any put not positvely matched by the function. Hence type
|
||||
// narrowing results in wrong results.
|
||||
return typeof obj === 'object'
|
||||
&& obj !== null
|
||||
&& !Array.isArray(obj)
|
||||
&& !(obj instanceof RegExp)
|
||||
&& !(obj instanceof Date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type
|
||||
*/
|
||||
export function isTypedArray(obj: unknown): obj is Object {
|
||||
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
||||
return typeof obj === 'object'
|
||||
&& obj instanceof TypedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* In **contrast** to just checking `typeof` this will return `false` for `NaN`.
|
||||
* @returns whether the provided parameter is a JavaScript Number or not.
|
||||
*/
|
||||
export function isNumber(obj: unknown): obj is number {
|
||||
return (typeof obj === 'number' && !isNaN(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is an Iterable, casting to the given generic
|
||||
*/
|
||||
export function isIterable<T>(obj: unknown): obj is Iterable<T> {
|
||||
return !!obj && typeof (obj as any)[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Boolean or not.
|
||||
*/
|
||||
export function isBoolean(obj: unknown): obj is boolean {
|
||||
return (obj === true || obj === false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined.
|
||||
*/
|
||||
export function isUndefined(obj: unknown): obj is undefined {
|
||||
return (typeof obj === 'undefined');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is defined.
|
||||
*/
|
||||
export function isDefined<T>(arg: T | null | undefined): arg is T {
|
||||
return !isUndefinedOrNull(arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is undefined or null.
|
||||
*/
|
||||
export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
|
||||
return (isUndefined(obj) || obj === null);
|
||||
}
|
||||
|
||||
|
||||
export function assertType(condition: unknown, type?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(type ? `Unexpected type, expected '${type}'` : 'Unexpected type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the argument passed in is neither undefined nor null.
|
||||
*
|
||||
* @see {@link assertDefined} for a similar utility that leverages TS assertion functions to narrow down the type of `arg` to be non-nullable.
|
||||
*/
|
||||
export function assertIsDefined<T>(arg: T | null | undefined): NonNullable<T> {
|
||||
assert(
|
||||
arg !== null && arg !== undefined,
|
||||
'Argument is `undefined` or `null`.',
|
||||
);
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a provided `value` is `defined` - not `null` or `undefined`,
|
||||
* throwing an error with the provided error or error message, while also
|
||||
* narrowing down the type of the `value` to be `NonNullable` using TS
|
||||
* assertion functions.
|
||||
*
|
||||
* @throws if the provided `value` is `null` or `undefined`.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* ```typescript
|
||||
* // an assert with an error message
|
||||
* assertDefined('some value', 'String constant is not defined o_O.');
|
||||
*
|
||||
* // `throws!` the provided error
|
||||
* assertDefined(null, new Error('Should throw this error.'));
|
||||
*
|
||||
* // narrows down the type of `someValue` to be non-nullable
|
||||
* const someValue: string | undefined | null = blackbox();
|
||||
* assertDefined(someValue, 'Some value must be defined.');
|
||||
* console.log(someValue.length); // now type of `someValue` is `string`
|
||||
* ```
|
||||
*
|
||||
* @see {@link assertIsDefined} for a similar utility but without assertion.
|
||||
* @see {@link https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions typescript-3-7.html#assertion-functions}
|
||||
*/
|
||||
export function assertDefined<T>(value: T, error: string | NonNullable<Error>): asserts value is NonNullable<T> {
|
||||
if (value === null || value === undefined) {
|
||||
const errorToThrow = typeof error === 'string' ? new Error(error) : error;
|
||||
|
||||
throw errorToThrow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that each argument passed in is neither undefined nor null.
|
||||
*/
|
||||
export function assertAllDefined<T1, T2>(t1: T1 | null | undefined, t2: T2 | null | undefined): [T1, T2];
|
||||
export function assertAllDefined<T1, T2, T3>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined): [T1, T2, T3];
|
||||
export function assertAllDefined<T1, T2, T3, T4>(t1: T1 | null | undefined, t2: T2 | null | undefined, t3: T3 | null | undefined, t4: T4 | null | undefined): [T1, T2, T3, T4];
|
||||
export function assertAllDefined(...args: (unknown | null | undefined)[]): unknown[] {
|
||||
const result:any = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (isUndefinedOrNull(arg)) {
|
||||
throw new Error(`Assertion Failed: argument at index ${i} is undefined or null`);
|
||||
}
|
||||
|
||||
result.push(arg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is an empty JavaScript Object or not.
|
||||
*/
|
||||
export function isEmptyObject(obj: unknown): obj is object {
|
||||
if (!isObject(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (hasOwnProperty.call(obj, key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameter is a JavaScript Function or not.
|
||||
*/
|
||||
export function isFunction(obj: unknown): obj is Function {
|
||||
return (typeof obj === 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the provided parameters is are JavaScript Function or not.
|
||||
*/
|
||||
export function areFunctions(...objects: unknown[]): boolean {
|
||||
return objects.length > 0 && objects.every(isFunction);
|
||||
}
|
||||
|
||||
export type TypeConstraint = string | Function;
|
||||
|
||||
export function validateConstraints(args: unknown[], constraints: Array<TypeConstraint | undefined>): void {
|
||||
const len = Math.min(args.length, constraints.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
validateConstraint(args[i], constraints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateConstraint(arg: unknown, constraint: TypeConstraint | undefined): void {
|
||||
|
||||
if (isString(constraint)) {
|
||||
if (typeof arg !== constraint) {
|
||||
throw new Error(`argument does not match constraint: typeof ${constraint}`);
|
||||
}
|
||||
} else if (isFunction(constraint)) {
|
||||
try {
|
||||
if (arg instanceof constraint) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!isUndefinedOrNull(arg) && (arg as any).constructor === constraint) {
|
||||
return;
|
||||
}
|
||||
if (constraint.length === 1 && constraint.call(undefined, arg) === true) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type assertion that safely upcasts a type to a supertype.
|
||||
*
|
||||
* This can be used to make sure the argument correctly conforms to the subtype while still being able to pass it
|
||||
* to contexts that expects the supertype.
|
||||
*/
|
||||
export function upcast<Base, Sub extends Base = Base>(x: Sub): Base {
|
||||
return x;
|
||||
}
|
||||
|
||||
type AddFirstParameterToFunction<T, TargetFunctionsReturnType, FirstParameter> = T extends (...args: any[]) => TargetFunctionsReturnType ?
|
||||
// Function: add param to function
|
||||
(firstArg: FirstParameter, ...args: Parameters<T>) => ReturnType<T> :
|
||||
|
||||
// Else: just leave as is
|
||||
T;
|
||||
|
||||
/**
|
||||
* Allows to add a first parameter to functions of a type.
|
||||
*/
|
||||
export type AddFirstParameterToFunctions<Target, TargetFunctionsReturnType, FirstParameter> = {
|
||||
// For every property
|
||||
[K in keyof Target]: AddFirstParameterToFunction<Target[K], TargetFunctionsReturnType, FirstParameter>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an object with all optional properties, requires at least one to be defined.
|
||||
* i.e. AtLeastOne<MyObject>;
|
||||
*/
|
||||
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
||||
|
||||
/**
|
||||
* Only picks the non-optional properties of a type.
|
||||
*/
|
||||
export type OmitOptional<T> = { [K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K] };
|
||||
|
||||
/**
|
||||
* A type that removed readonly-less from all properties of `T`
|
||||
*/
|
||||
export type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
};
|
||||
|
||||
/**
|
||||
* A single object or an array of the objects.
|
||||
*/
|
||||
export type SingleOrMany<T> = T | T[];
|
||||
|
||||
|
||||
/**
|
||||
* A type that recursively makes all properties of `T` required
|
||||
*/
|
||||
export type DeepRequiredNonNullable<T> = {
|
||||
[P in keyof T]-?: T[P] extends object ? DeepRequiredNonNullable<T[P]> : Required<NonNullable<T[P]>>;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Represents a type that is a partial version of a given type `T`, where all properties are optional and can be deeply nested.
|
||||
*/
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : Partial<T[P]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a type that is a partial version of a given type `T`, except a subset.
|
||||
*/
|
||||
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
|
||||
59
packages/core/src/uint.ts
Normal file
59
packages/core/src/uint.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const enum Constants {
|
||||
/**
|
||||
* MAX SMI (SMall Integer) as defined in v8.
|
||||
* one bit is lost for boxing/unboxing flag.
|
||||
* one bit is lost for sign flag.
|
||||
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
|
||||
*/
|
||||
MAX_SAFE_SMALL_INTEGER = 1 << 30,
|
||||
|
||||
/**
|
||||
* MIN SMI (SMall Integer) as defined in v8.
|
||||
* one bit is lost for boxing/unboxing flag.
|
||||
* one bit is lost for sign flag.
|
||||
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
|
||||
*/
|
||||
MIN_SAFE_SMALL_INTEGER = -(1 << 30),
|
||||
|
||||
/**
|
||||
* Max unsigned integer that fits on 8 bits.
|
||||
*/
|
||||
MAX_UINT_8 = 255, // 2^8 - 1
|
||||
|
||||
/**
|
||||
* Max unsigned integer that fits on 16 bits.
|
||||
*/
|
||||
MAX_UINT_16 = 65535, // 2^16 - 1
|
||||
|
||||
/**
|
||||
* Max unsigned integer that fits on 32 bits.
|
||||
*/
|
||||
MAX_UINT_32 = 4294967295, // 2^32 - 1
|
||||
|
||||
UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000
|
||||
}
|
||||
|
||||
export function toUint8(v: number): number {
|
||||
if (v < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (v > Constants.MAX_UINT_8) {
|
||||
return Constants.MAX_UINT_8;
|
||||
}
|
||||
return v | 0;
|
||||
}
|
||||
|
||||
export function toUint32(v: number): number {
|
||||
if (v < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (v > Constants.MAX_UINT_32) {
|
||||
return Constants.MAX_UINT_32;
|
||||
}
|
||||
return v | 0;
|
||||
}
|
||||
750
packages/core/src/uri.ts
Normal file
750
packages/core/src/uri.ts
Normal file
@ -0,0 +1,750 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from './charCode.js';
|
||||
import { MarshalledId } from './marshallingIds.js';
|
||||
import * as paths from './path.js';
|
||||
import { isWindows } from './platform.js';
|
||||
|
||||
const _schemePattern = /^\w[\w\d+.-]*$/;
|
||||
const _singleSlashStart = /^\//;
|
||||
const _doubleSlashStart = /^\/\//;
|
||||
|
||||
function _validateUri(ret: URI, _strict?: boolean): void {
|
||||
|
||||
// scheme, must be set
|
||||
if (!ret.scheme && _strict) {
|
||||
throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`);
|
||||
}
|
||||
|
||||
// scheme, https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
if (ret.scheme && !_schemePattern.test(ret.scheme)) {
|
||||
throw new Error('[UriError]: Scheme contains illegal characters.');
|
||||
}
|
||||
|
||||
// path, http://tools.ietf.org/html/rfc3986#section-3.3
|
||||
// If a URI contains an authority component, then the path component
|
||||
// must either be empty or begin with a slash ("/") character. If a URI
|
||||
// does not contain an authority component, then the path cannot begin
|
||||
// with two slash characters ("//").
|
||||
if (ret.path) {
|
||||
if (ret.authority) {
|
||||
if (!_singleSlashStart.test(ret.path)) {
|
||||
throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character');
|
||||
}
|
||||
} else {
|
||||
if (_doubleSlashStart.test(ret.path)) {
|
||||
throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for a while we allowed uris *without* schemes and this is the migration
|
||||
// for them, e.g. an uri without scheme and without strict-mode warns and falls
|
||||
// back to the file-scheme. that should cause the least carnage and still be a
|
||||
// clear warning
|
||||
function _schemeFix(scheme: string, _strict: boolean): string {
|
||||
if (!scheme && !_strict) {
|
||||
return 'file';
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
// implements a bit of https://tools.ietf.org/html/rfc3986#section-5
|
||||
function _referenceResolution(scheme: string, path: string): string {
|
||||
|
||||
// the slash-character is our 'default base' as we don't
|
||||
// support constructing URIs relative to other URIs. This
|
||||
// also means that we alter and potentially break paths.
|
||||
// see https://tools.ietf.org/html/rfc3986#section-5.1.4
|
||||
switch (scheme) {
|
||||
case 'https':
|
||||
case 'http':
|
||||
case 'file':
|
||||
if (!path) {
|
||||
path = _slash;
|
||||
} else if (path[0] !== _slash) {
|
||||
path = _slash + path;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
/**
|
||||
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
|
||||
* This class is a simple parser which creates the basic component parts
|
||||
* (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
|
||||
* and encoding.
|
||||
*
|
||||
* ```txt
|
||||
* foo://example.com:8042/over/there?name=ferret#nose
|
||||
* \_/ \______________/\_________/ \_________/ \__/
|
||||
* | | | | |
|
||||
* scheme authority path query fragment
|
||||
* | _____________________|__
|
||||
* / \ / \
|
||||
* urn:example:animal:ferret:nose
|
||||
* ```
|
||||
*/
|
||||
export class URI implements UriComponents {
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (thing instanceof URI) {
|
||||
return true;
|
||||
}
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return typeof (<URI>thing).authority === 'string'
|
||||
&& typeof (<URI>thing).fragment === 'string'
|
||||
&& typeof (<URI>thing).path === 'string'
|
||||
&& typeof (<URI>thing).query === 'string'
|
||||
&& typeof (<URI>thing).scheme === 'string'
|
||||
&& typeof (<URI>thing).fsPath === 'string'
|
||||
&& typeof (<URI>thing).with === 'function'
|
||||
&& typeof (<URI>thing).toString === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* scheme is the 'http' part of 'http://www.example.com/some/path?query#fragment'.
|
||||
* The part before the first colon.
|
||||
*/
|
||||
readonly scheme: string;
|
||||
|
||||
/**
|
||||
* authority is the 'www.example.com' part of 'http://www.example.com/some/path?query#fragment'.
|
||||
* The part between the first double slashes and the next slash.
|
||||
*/
|
||||
readonly authority: string;
|
||||
|
||||
/**
|
||||
* path is the '/some/path' part of 'http://www.example.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* query is the 'query' part of 'http://www.example.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly query: string;
|
||||
|
||||
/**
|
||||
* fragment is the 'fragment' part of 'http://www.example.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly fragment: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string, _strict?: boolean);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(components: UriComponents);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string, _strict: boolean = false) {
|
||||
|
||||
if (typeof schemeOrData === 'object') {
|
||||
this.scheme = schemeOrData.scheme || _empty;
|
||||
this.authority = schemeOrData.authority || _empty;
|
||||
this.path = schemeOrData.path || _empty;
|
||||
this.query = schemeOrData.query || _empty;
|
||||
this.fragment = schemeOrData.fragment || _empty;
|
||||
// no validation because it's this URI
|
||||
// that creates uri components.
|
||||
// _validateUri(this);
|
||||
} else {
|
||||
this.scheme = _schemeFix(schemeOrData, _strict);
|
||||
this.authority = authority || _empty;
|
||||
this.path = _referenceResolution(this.scheme, path || _empty);
|
||||
this.query = query || _empty;
|
||||
this.fragment = fragment || _empty;
|
||||
|
||||
_validateUri(this, _strict);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- filesystem path -----------------------
|
||||
|
||||
/**
|
||||
* Returns a string representing the corresponding file system path of this URI.
|
||||
* Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
|
||||
* platform specific path separator.
|
||||
*
|
||||
* * Will *not* validate the path for invalid characters and semantics.
|
||||
* * Will *not* look at the scheme of this URI.
|
||||
* * The result shall *not* be used for display purposes but for accessing a file on disk.
|
||||
*
|
||||
*
|
||||
* The *difference* to `URI#path` is the use of the platform specific separator and the handling
|
||||
* of UNC paths. See the below sample of a file-uri with an authority (UNC path).
|
||||
*
|
||||
* ```ts
|
||||
const u = URI.parse('file://server/c$/folder/file.txt')
|
||||
u.authority === 'server'
|
||||
u.path === '/shares/c$/file.txt'
|
||||
u.fsPath === '\\server\c$\folder\file.txt'
|
||||
```
|
||||
*
|
||||
* Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
|
||||
* namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
|
||||
* with URIs that represent files on disk (`file` scheme).
|
||||
*/
|
||||
get fsPath(): string {
|
||||
// if (this.scheme !== 'file') {
|
||||
// console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
|
||||
// }
|
||||
return uriToFsPath(this, false);
|
||||
}
|
||||
|
||||
// ---- modify to new -------------------------
|
||||
|
||||
with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI {
|
||||
|
||||
if (!change) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let { scheme, authority, path, query, fragment } = change;
|
||||
if (scheme === undefined) {
|
||||
scheme = this.scheme;
|
||||
} else if (scheme === null) {
|
||||
scheme = _empty;
|
||||
}
|
||||
if (authority === undefined) {
|
||||
authority = this.authority;
|
||||
} else if (authority === null) {
|
||||
authority = _empty;
|
||||
}
|
||||
if (path === undefined) {
|
||||
path = this.path;
|
||||
} else if (path === null) {
|
||||
path = _empty;
|
||||
}
|
||||
if (query === undefined) {
|
||||
query = this.query;
|
||||
} else if (query === null) {
|
||||
query = _empty;
|
||||
}
|
||||
if (fragment === undefined) {
|
||||
fragment = this.fragment;
|
||||
} else if (fragment === null) {
|
||||
fragment = _empty;
|
||||
}
|
||||
|
||||
if (scheme === this.scheme
|
||||
&& authority === this.authority
|
||||
&& path === this.path
|
||||
&& query === this.query
|
||||
&& fragment === this.fragment) {
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
return new Uri(scheme, authority, path, query, fragment);
|
||||
}
|
||||
|
||||
// ---- parse & validate ------------------------
|
||||
|
||||
/**
|
||||
* Creates a new URI from a string, e.g. `http://www.example.com/some/path`,
|
||||
* `file:///usr/home`, or `scheme:with/path`.
|
||||
*
|
||||
* @param value A string which represents an URI (see `URI#toString`).
|
||||
*/
|
||||
static parse(value: string, _strict: boolean = false): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return new Uri(_empty, _empty, _empty, _empty, _empty);
|
||||
}
|
||||
return new Uri(
|
||||
match[2] || _empty,
|
||||
percentDecode(match[4] || _empty),
|
||||
percentDecode(match[5] || _empty),
|
||||
percentDecode(match[7] || _empty),
|
||||
percentDecode(match[9] || _empty),
|
||||
_strict
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new URI from a file system path, e.g. `c:\my\files`,
|
||||
* `/usr/home`, or `\\server\share\some\path`.
|
||||
*
|
||||
* The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
|
||||
* as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
|
||||
* `URI.parse('file://' + path)` because the path might contain characters that are
|
||||
* interpreted (# and ?). See the following sample:
|
||||
* ```ts
|
||||
const good = URI.file('/coding/c#/project1');
|
||||
good.scheme === 'file';
|
||||
good.path === '/coding/c#/project1';
|
||||
good.fragment === '';
|
||||
const bad = URI.parse('file://' + '/coding/c#/project1');
|
||||
bad.scheme === 'file';
|
||||
bad.path === '/coding/c'; // path is now broken
|
||||
bad.fragment === '/project1';
|
||||
```
|
||||
*
|
||||
* @param path A file system path (see `URI#fsPath`)
|
||||
*/
|
||||
static file(path: string): URI {
|
||||
|
||||
let authority = _empty;
|
||||
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if (isWindows) {
|
||||
path = path.replace(/\\/g, _slash);
|
||||
}
|
||||
|
||||
// check for authority as used in UNC shares
|
||||
// or use the path as given
|
||||
if (path[0] === _slash && path[1] === _slash) {
|
||||
const idx = path.indexOf(_slash, 2);
|
||||
if (idx === -1) {
|
||||
authority = path.substring(2);
|
||||
path = _slash;
|
||||
} else {
|
||||
authority = path.substring(2, idx);
|
||||
path = path.substring(idx) || _slash;
|
||||
}
|
||||
}
|
||||
|
||||
return new Uri('file', authority, path, _empty, _empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new URI from uri components.
|
||||
*
|
||||
* Unless `strict` is `true` the scheme is defaults to be `file`. This function performs
|
||||
* validation and should be used for untrusted uri components retrieved from storage,
|
||||
* user input, command arguments etc
|
||||
*/
|
||||
static from(components: UriComponents, strict?: boolean): URI {
|
||||
const result = new Uri(
|
||||
components.scheme,
|
||||
components.authority,
|
||||
components.path,
|
||||
components.query,
|
||||
components.fragment,
|
||||
strict
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param uri The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
static joinPath(uri: URI, ...pathFragment: string[]): URI {
|
||||
if (!uri.path) {
|
||||
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
|
||||
}
|
||||
let newPath: string;
|
||||
if (isWindows && uri.scheme === 'file') {
|
||||
newPath = URI.file(paths.win32.join(uriToFsPath(uri, true), ...pathFragment)).path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return uri.with({ path: newPath });
|
||||
}
|
||||
|
||||
// ---- printing/externalize ---------------------------
|
||||
|
||||
/**
|
||||
* Creates a string representation for this URI. It's guaranteed that calling
|
||||
* `URI.parse` with the result of this function creates an URI which is equal
|
||||
* to this URI.
|
||||
*
|
||||
* * The result shall *not* be used for display purposes but for externalization or transport.
|
||||
* * The result will be encoded using the percentage encoding and encoding happens mostly
|
||||
* ignore the scheme-specific encoding rules.
|
||||
*
|
||||
* @param skipEncoding Do not encode the result, default is `false`
|
||||
*/
|
||||
toString(skipEncoding: boolean = false): string {
|
||||
return _asFormatted(this, skipEncoding);
|
||||
}
|
||||
|
||||
toJSON(): UriComponents {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to revive URIs.
|
||||
*
|
||||
* **Note** that this function should only be used when receiving URI#toJSON generated data
|
||||
* and that it doesn't do any validation. Use {@link URI.from} when received "untrusted"
|
||||
* uri components such as command arguments or data from storage.
|
||||
*
|
||||
* @param data The URI components or URI to revive.
|
||||
* @returns The revived URI or undefined or null.
|
||||
*/
|
||||
static revive(data: UriComponents | URI): URI;
|
||||
static revive(data: UriComponents | URI | undefined): URI | undefined;
|
||||
static revive(data: UriComponents | URI | null): URI | null;
|
||||
static revive(data: UriComponents | URI | undefined | null): URI | undefined | null;
|
||||
static revive(data: UriComponents | URI | undefined | null): URI | undefined | null {
|
||||
if (!data) {
|
||||
return data as undefined | null;
|
||||
} else if (data instanceof URI) {
|
||||
return data;
|
||||
} else {
|
||||
const result = new Uri(data);
|
||||
result._formatted = (<UriState>data).external ?? null;
|
||||
result._fsPath = (<UriState>data)._sep === _pathSepMarker ? (<UriState>data).fsPath ?? null : null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.for('debug.description')]() {
|
||||
return `URI(${this.toString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UriComponents {
|
||||
scheme: string;
|
||||
authority?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
export function isUriComponents(thing: any): thing is UriComponents {
|
||||
if (!thing || typeof thing !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return typeof (<UriComponents>thing).scheme === 'string'
|
||||
&& (typeof (<UriComponents>thing).authority === 'string' || typeof (<UriComponents>thing).authority === 'undefined')
|
||||
&& (typeof (<UriComponents>thing).path === 'string' || typeof (<UriComponents>thing).path === 'undefined')
|
||||
&& (typeof (<UriComponents>thing).query === 'string' || typeof (<UriComponents>thing).query === 'undefined')
|
||||
&& (typeof (<UriComponents>thing).fragment === 'string' || typeof (<UriComponents>thing).fragment === 'undefined');
|
||||
}
|
||||
|
||||
interface UriState extends UriComponents {
|
||||
$mid: MarshalledId.Uri;
|
||||
external?: string;
|
||||
fsPath?: string;
|
||||
_sep?: 1;
|
||||
}
|
||||
|
||||
const _pathSepMarker = isWindows ? 1 : undefined;
|
||||
|
||||
// This class exists so that URI is compatible with vscode.Uri (API).
|
||||
class Uri extends URI {
|
||||
|
||||
_formatted: string | null = null;
|
||||
_fsPath: string | null = null;
|
||||
|
||||
override get fsPath(): string {
|
||||
if (!this._fsPath) {
|
||||
this._fsPath = uriToFsPath(this, false);
|
||||
}
|
||||
return this._fsPath;
|
||||
}
|
||||
|
||||
override toString(skipEncoding: boolean = false): string {
|
||||
if (!skipEncoding) {
|
||||
if (!this._formatted) {
|
||||
this._formatted = _asFormatted(this, false);
|
||||
}
|
||||
return this._formatted;
|
||||
} else {
|
||||
// we don't cache that
|
||||
return _asFormatted(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
override toJSON(): UriComponents {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
const res = <UriState>{
|
||||
$mid: MarshalledId.Uri
|
||||
};
|
||||
// cached state
|
||||
if (this._fsPath) {
|
||||
res.fsPath = this._fsPath;
|
||||
res._sep = _pathSepMarker;
|
||||
}
|
||||
if (this._formatted) {
|
||||
res.external = this._formatted;
|
||||
}
|
||||
//--- uri components
|
||||
if (this.path) {
|
||||
res.path = this.path;
|
||||
}
|
||||
// TODO
|
||||
// this isn't correct and can violate the UriComponents contract but
|
||||
// this is part of the vscode.Uri API and we shouldn't change how that
|
||||
// works anymore
|
||||
if (this.scheme) {
|
||||
res.scheme = this.scheme;
|
||||
}
|
||||
if (this.authority) {
|
||||
res.authority = this.authority;
|
||||
}
|
||||
if (this.query) {
|
||||
res.query = this.query;
|
||||
}
|
||||
if (this.fragment) {
|
||||
res.fragment = this.fragment;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
const encodeTable: { [ch: number]: string } = {
|
||||
[CharCode.Colon]: '%3A', // gen-delims
|
||||
[CharCode.Slash]: '%2F',
|
||||
[CharCode.QuestionMark]: '%3F',
|
||||
[CharCode.Hash]: '%23',
|
||||
[CharCode.OpenSquareBracket]: '%5B',
|
||||
[CharCode.CloseSquareBracket]: '%5D',
|
||||
[CharCode.AtSign]: '%40',
|
||||
|
||||
[CharCode.ExclamationMark]: '%21', // sub-delims
|
||||
[CharCode.DollarSign]: '%24',
|
||||
[CharCode.Ampersand]: '%26',
|
||||
[CharCode.SingleQuote]: '%27',
|
||||
[CharCode.OpenParen]: '%28',
|
||||
[CharCode.CloseParen]: '%29',
|
||||
[CharCode.Asterisk]: '%2A',
|
||||
[CharCode.Plus]: '%2B',
|
||||
[CharCode.Comma]: '%2C',
|
||||
[CharCode.Semicolon]: '%3B',
|
||||
[CharCode.Equals]: '%3D',
|
||||
|
||||
[CharCode.Space]: '%20',
|
||||
};
|
||||
|
||||
function encodeURIComponentFast(uriComponent: string, isPath: boolean, isAuthority: boolean): string {
|
||||
let res: string | undefined = undefined;
|
||||
let nativeEncodePos = -1;
|
||||
|
||||
for (let pos = 0; pos < uriComponent.length; pos++) {
|
||||
const code = uriComponent.charCodeAt(pos);
|
||||
|
||||
// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
if (
|
||||
(code >= CharCode.a && code <= CharCode.z)
|
||||
|| (code >= CharCode.A && code <= CharCode.Z)
|
||||
|| (code >= CharCode.Digit0 && code <= CharCode.Digit9)
|
||||
|| code === CharCode.Dash
|
||||
|| code === CharCode.Period
|
||||
|| code === CharCode.Underline
|
||||
|| code === CharCode.Tilde
|
||||
|| (isPath && code === CharCode.Slash)
|
||||
|| (isAuthority && code === CharCode.OpenSquareBracket)
|
||||
|| (isAuthority && code === CharCode.CloseSquareBracket)
|
||||
|| (isAuthority && code === CharCode.Colon)
|
||||
) {
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
// check if we write into a new string (by default we try to return the param)
|
||||
if (res !== undefined) {
|
||||
res += uriComponent.charAt(pos);
|
||||
}
|
||||
|
||||
} else {
|
||||
// encoding needed, we need to allocate a new string
|
||||
if (res === undefined) {
|
||||
res = uriComponent.substr(0, pos);
|
||||
}
|
||||
|
||||
// check with default table first
|
||||
const escaped = encodeTable[code];
|
||||
if (escaped !== undefined) {
|
||||
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
|
||||
// append escaped variant to result
|
||||
res += escaped;
|
||||
|
||||
} else if (nativeEncodePos === -1) {
|
||||
// use native encode only when needed
|
||||
nativeEncodePos = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
|
||||
}
|
||||
|
||||
return res !== undefined ? res : uriComponent;
|
||||
}
|
||||
|
||||
function encodeURIComponentMinimal(path: string): string {
|
||||
let res: string | undefined = undefined;
|
||||
for (let pos = 0; pos < path.length; pos++) {
|
||||
const code = path.charCodeAt(pos);
|
||||
if (code === CharCode.Hash || code === CharCode.QuestionMark) {
|
||||
if (res === undefined) {
|
||||
res = path.substr(0, pos);
|
||||
}
|
||||
res += encodeTable[code];
|
||||
} else {
|
||||
if (res !== undefined) {
|
||||
res += path[pos];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res !== undefined ? res : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `fsPath` for the given uri
|
||||
*/
|
||||
export function uriToFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
|
||||
|
||||
let value: string;
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`;
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash
|
||||
&& (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)
|
||||
&& uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2);
|
||||
} else {
|
||||
value = uri.path.substr(1);
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path;
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, '\\');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the external version of a uri
|
||||
*/
|
||||
function _asFormatted(uri: URI, skipEncoding: boolean): string {
|
||||
|
||||
const encoder = !skipEncoding
|
||||
? encodeURIComponentFast
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
res += ':';
|
||||
}
|
||||
if (authority || scheme === 'file') {
|
||||
res += _slash;
|
||||
res += _slash;
|
||||
}
|
||||
if (authority) {
|
||||
let idx = authority.indexOf('@');
|
||||
if (idx !== -1) {
|
||||
// <user>@<auth>
|
||||
const userinfo = authority.substr(0, idx);
|
||||
authority = authority.substr(idx + 1);
|
||||
idx = userinfo.lastIndexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(userinfo, false, false);
|
||||
} else {
|
||||
// <user>:<pass>@<auth>
|
||||
res += encoder(userinfo.substr(0, idx), false, false);
|
||||
res += ':';
|
||||
res += encoder(userinfo.substr(idx + 1), false, true);
|
||||
}
|
||||
res += '@';
|
||||
}
|
||||
authority = authority.toLowerCase();
|
||||
idx = authority.lastIndexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(authority, false, true);
|
||||
} else {
|
||||
// <auth>:<port>
|
||||
res += encoder(authority.substr(0, idx), false, true);
|
||||
res += authority.substr(idx);
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
// lower-case windows drive letters in /C:/fff or C:/fff
|
||||
if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) {
|
||||
const code = path.charCodeAt(1);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
|
||||
}
|
||||
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
|
||||
const code = path.charCodeAt(0);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
|
||||
}
|
||||
}
|
||||
// encode the rest of the path
|
||||
res += encoder(path, true, false);
|
||||
}
|
||||
if (query) {
|
||||
res += '?';
|
||||
res += encoder(query, false, false);
|
||||
}
|
||||
if (fragment) {
|
||||
res += '#';
|
||||
res += !skipEncoding ? encodeURIComponentFast(fragment, false, false) : fragment;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// --- decode
|
||||
|
||||
function decodeURIComponentGraceful(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
if (str.length > 3) {
|
||||
return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3));
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
|
||||
|
||||
function percentDecode(str: string): string {
|
||||
if (!str.match(_rEncodedAsHex)) {
|
||||
return str;
|
||||
}
|
||||
return str.replace(_rEncodedAsHex, (match) => decodeURIComponentGraceful(match));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped-type that replaces all occurrences of URI with UriComponents
|
||||
*/
|
||||
export type UriDto<T> = { [K in keyof T]: T[K] extends URI
|
||||
? UriComponents
|
||||
: UriDto<T[K]> };
|
||||
443
packages/core/src/utils.ts
Normal file
443
packages/core/src/utils.ts
Normal file
@ -0,0 +1,443 @@
|
||||
export function StingEnum<T extends string>(o: Array<T>): { [K in T]: K } {
|
||||
return o.reduce((res, key) => {
|
||||
res[key] = key;
|
||||
return res;
|
||||
}, Object.create(null));
|
||||
}
|
||||
|
||||
import { isString } from './primitives.js';
|
||||
|
||||
const escapeRegExpPattern = /[[\]{}()|/\\^$.*+?]/g;
|
||||
const escapeXmlPattern = /[&<]/g;
|
||||
const escapeXmlForPattern = /[&<>'"]/g;
|
||||
const escapeXmlMap: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'\'': '''
|
||||
};
|
||||
export const DefaultDelimiter = {
|
||||
begin: '<%',
|
||||
end: '%>'
|
||||
};
|
||||
export const hasFlag = (field, enumValue) => {
|
||||
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
|
||||
return ((1 << enumValue) & field) ? true : false;
|
||||
};
|
||||
export const hasFlagHex = (field, enumValue) => {
|
||||
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
|
||||
return enumValue & field ? true : false;
|
||||
};
|
||||
export const disableFlag = (enumValue, field) => {
|
||||
enumValue &= ~(1 << field);
|
||||
return enumValue;
|
||||
};
|
||||
/**
|
||||
* The minimum location of high surrogates
|
||||
*/
|
||||
export const HIGH_SURROGATE_MIN = 0xD800;
|
||||
/**
|
||||
* The maximum location of high surrogates
|
||||
*/
|
||||
export const HIGH_SURROGATE_MAX = 0xDBFF;
|
||||
/**
|
||||
* The minimum location of low surrogates
|
||||
*/
|
||||
export const LOW_SURROGATE_MIN = 0xDC00;
|
||||
/**
|
||||
* The maximum location of low surrogates
|
||||
*/
|
||||
export const LOW_SURROGATE_MAX = 0xDFFF;
|
||||
|
||||
const BASE64_KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
|
||||
export const capitalize = (word) => {
|
||||
return word.substring(0, 1).toUpperCase() + word.substring(1);
|
||||
};
|
||||
|
||||
export const getJson = (inData, validOnly, ommit) => {
|
||||
try {
|
||||
return isString(inData) ? JSON.parse(inData) : validOnly === true ? null : inData;
|
||||
} catch (e) {
|
||||
ommit !== false && console.error('error parsing json data ' + inData + ' error = ' + e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes a string so that it can safely be passed to the RegExp constructor.
|
||||
* @param text The string to be escaped
|
||||
* @return The escaped string
|
||||
*/
|
||||
export function escapeRegExpEx(text: string): string {
|
||||
return !text ? text : text.replace(escapeRegExpPattern, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string to protect against tag injection.
|
||||
* @param xml The string to be escaped
|
||||
* @param forAttribute Whether to also escape ', ", and > in addition to < and &
|
||||
* @return The escaped string
|
||||
*/
|
||||
export function escapeXml(xml: string, forAttribute = true): string {
|
||||
if (!xml) {
|
||||
return xml;
|
||||
}
|
||||
|
||||
const pattern = forAttribute ? escapeXmlForPattern : escapeXmlPattern;
|
||||
|
||||
return xml.replace(pattern, function (character: string): string {
|
||||
return escapeXmlMap[character];
|
||||
});
|
||||
}
|
||||
|
||||
export function createUUID(): string {
|
||||
const S4 = function () {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
};
|
||||
return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
|
||||
}
|
||||
|
||||
export function escapeRegExp(str: string): string {
|
||||
const special = ['[', ']', '(', ')', '{', '}', '*', '+', '.', '|', '||'];
|
||||
for (let n = 0; n < special.length; n++) {
|
||||
str = str.replace(special[n], '\\' + special[n]);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function replaceAll(find: string, replace: string, str: string): string {
|
||||
return str ? str.split(find).join(replace) : '';
|
||||
};
|
||||
|
||||
export interface IDelimiter {
|
||||
begin: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export const substitute = (template, map) => {
|
||||
const transform = (k) => k || '';
|
||||
return template.replace(/\$\{([^\s:}]+)(?::([^\s:}]+))?\}/g,
|
||||
(match, key, format) => transform(map[key]).toString());
|
||||
}
|
||||
|
||||
function decodeUtf8EncodedCodePoint(codePoint: number, validationRange: number[] = [0, Infinity], checkSurrogate?: boolean): string {
|
||||
if (codePoint < validationRange[0] || codePoint > validationRange[1]) {
|
||||
throw Error('Invalid continuation byte');
|
||||
}
|
||||
|
||||
if (checkSurrogate && codePoint >= HIGH_SURROGATE_MIN && codePoint <= LOW_SURROGATE_MAX) {
|
||||
throw Error('Surrogate is not a scalar value');
|
||||
}
|
||||
|
||||
let encoded = '';
|
||||
|
||||
if (codePoint > 0xFFFF) {
|
||||
codePoint -= 0x010000;
|
||||
encoded += String.fromCharCode(codePoint >>> 0x10 & 0x03FF | HIGH_SURROGATE_MIN);
|
||||
codePoint = LOW_SURROGATE_MIN | codePoint & 0x03FF;
|
||||
}
|
||||
|
||||
encoded += String.fromCharCode(codePoint);
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
function validateUtf8EncodedCodePoint(codePoint: number): void {
|
||||
if ((codePoint & 0xC0) !== 0x80) {
|
||||
throw Error('Invalid continuation byte');
|
||||
}
|
||||
}
|
||||
|
||||
export type ByteBuffer = Uint16Array | Uint8Array | Buffer | number[];
|
||||
|
||||
export interface Codec {
|
||||
encode(data: string): number[];
|
||||
decode(data: ByteBuffer): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides facilities for encoding a string into an ASCII-encoded byte buffer and
|
||||
* decoding an ASCII-encoded byte buffer into a string.
|
||||
*/
|
||||
export const ascii: Codec = {
|
||||
/**
|
||||
* Encodes a string into an ASCII-encoded byte buffer.
|
||||
*
|
||||
* @param data The text string to encode
|
||||
*/
|
||||
encode(data: string): number[] {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const buffer: number[] = [];
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i++) {
|
||||
buffer[i] = data.charCodeAt(i);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
},
|
||||
/**
|
||||
* Decodes an ASCII-encoded byte buffer into a string.
|
||||
*
|
||||
* @param data The byte buffer to decode
|
||||
*/
|
||||
decode(data: ByteBuffer): string {
|
||||
if (data == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let decoded = '';
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i++) {
|
||||
decoded += String.fromCharCode(data[i]);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides facilities for encoding a string into a Base64-encoded byte buffer and
|
||||
* decoding a Base64-encoded byte buffer into a string.
|
||||
*/
|
||||
export const base64: Codec = {
|
||||
/**
|
||||
* Encodes a Base64-encoded string into a Base64 byte buffer.
|
||||
*
|
||||
* @param data The Base64-encoded string to encode
|
||||
*/
|
||||
encode(data: string): number[] {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const buffer: number[] = [];
|
||||
|
||||
let i = 0;
|
||||
let length = data.length;
|
||||
|
||||
while (data[--length] === '=') { }
|
||||
while (i < length) {
|
||||
let encoded = BASE64_KEYSTR.indexOf(data[i++]) << 18;
|
||||
if (i <= length) {
|
||||
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 12;
|
||||
}
|
||||
if (i <= length) {
|
||||
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 6;
|
||||
}
|
||||
if (i <= length) {
|
||||
encoded |= BASE64_KEYSTR.indexOf(data[i++]);
|
||||
}
|
||||
|
||||
buffer.push((encoded >>> 16) & 0xff);
|
||||
buffer.push((encoded >>> 8) & 0xff);
|
||||
buffer.push(encoded & 0xff);
|
||||
}
|
||||
|
||||
while (buffer[buffer.length - 1] === 0) {
|
||||
buffer.pop();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
},
|
||||
/**
|
||||
* Decodes a Base64-encoded byte buffer into a Base64-encoded string.
|
||||
*
|
||||
* @param data The byte buffer to decode
|
||||
*/
|
||||
decode(data: ByteBuffer): string {
|
||||
if (data == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let decoded = '';
|
||||
let i = 0;
|
||||
|
||||
for (let length = data.length - (data.length % 3); i < length;) {
|
||||
const encoded = data[i++] << 16 | data[i++] << 8 | data[i++];
|
||||
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3F);
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3F);
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3F);
|
||||
decoded += BASE64_KEYSTR.charAt(encoded & 0x3F);
|
||||
}
|
||||
|
||||
if (data.length % 3 === 1) {
|
||||
const encoded = data[i++] << 16;
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
|
||||
decoded += '==';
|
||||
} else if (data.length % 3 === 2) {
|
||||
const encoded = data[i++] << 16 | data[i++] << 8;
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
|
||||
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3f);
|
||||
decoded += '=';
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides facilities for encoding a string into a hex-encoded byte buffer and
|
||||
* decoding a hex-encoded byte buffer into a string.
|
||||
*/
|
||||
export const hex: Codec = {
|
||||
/**
|
||||
* Encodes a string into a hex-encoded byte buffer.
|
||||
*
|
||||
* @param data The hex-encoded string to encode
|
||||
*/
|
||||
encode(data: string): number[] {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const buffer: number[] = [];
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i += 2) {
|
||||
const encodedChar = parseInt(data.substr(i, 2), 16);
|
||||
|
||||
buffer.push(encodedChar);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
},
|
||||
/**
|
||||
* Decodes a hex-encoded byte buffer into a hex-encoded string.
|
||||
*
|
||||
* @param data The byte buffer to decode
|
||||
*/
|
||||
decode(data: ByteBuffer): string {
|
||||
if (data == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let decoded = '';
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i++) {
|
||||
decoded += data[i].toString(16).toUpperCase();
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides facilities for encoding a string into a UTF-8-encoded byte buffer and
|
||||
* decoding a UTF-8-encoded byte buffer into a string.
|
||||
* Inspired by the work of: https://github.com/mathiasbynens/utf8.js
|
||||
*/
|
||||
export const utf8: Codec = {
|
||||
/**
|
||||
* Encodes a string into a UTF-8-encoded byte buffer.
|
||||
*
|
||||
* @param data The text string to encode
|
||||
*/
|
||||
encode(data: string): number[] {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const buffer: number[] = [];
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i++) {
|
||||
let encodedChar = data.charCodeAt(i);
|
||||
/**
|
||||
* Surrogates
|
||||
* http://en.wikipedia.org/wiki/Universal_Character_Set_characters
|
||||
*/
|
||||
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= HIGH_SURROGATE_MAX) {
|
||||
const lowSurrogate = data.charCodeAt(i + 1);
|
||||
if (lowSurrogate >= LOW_SURROGATE_MIN && lowSurrogate <= LOW_SURROGATE_MAX) {
|
||||
encodedChar = 0x010000 + (encodedChar - HIGH_SURROGATE_MIN) * 0x0400 + (lowSurrogate - LOW_SURROGATE_MIN);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (encodedChar < 0x80) {
|
||||
buffer.push(encodedChar);
|
||||
} else {
|
||||
if (encodedChar < 0x800) {
|
||||
buffer.push(((encodedChar >> 0x06) & 0x1F) | 0xC0);
|
||||
} else if (encodedChar < 0x010000) {
|
||||
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= LOW_SURROGATE_MAX) {
|
||||
throw Error('Surrogate is not a scalar value');
|
||||
}
|
||||
|
||||
buffer.push(((encodedChar >> 0x0C) & 0x0F) | 0xE0);
|
||||
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
|
||||
} else if (encodedChar < 0x200000) {
|
||||
buffer.push(((encodedChar >> 0x12) & 0x07) | 0xF0);
|
||||
buffer.push(((encodedChar >> 0x0C) & 0x3F) | 0x80);
|
||||
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
|
||||
}
|
||||
buffer.push((encodedChar & 0x3F) | 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
},
|
||||
/**
|
||||
* Decodes a UTF-8-encoded byte buffer into a string.
|
||||
*
|
||||
* @param data The byte buffer to decode
|
||||
*/
|
||||
decode(data: ByteBuffer): string {
|
||||
if (data == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let decoded = '';
|
||||
|
||||
for (let i = 0, length = data.length; i < length; i++) {
|
||||
const byte1 = data[i] & 0xFF;
|
||||
|
||||
if ((byte1 & 0x80) === 0) {
|
||||
decoded += decodeUtf8EncodedCodePoint(byte1);
|
||||
} else if ((byte1 & 0xE0) === 0xC0) {
|
||||
let byte2 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte2);
|
||||
byte2 = byte2 & 0x3F;
|
||||
const encodedByte = ((byte1 & 0x1F) << 0x06) | byte2;
|
||||
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x80, Infinity]);
|
||||
} else if ((byte1 & 0xF0) === 0xE0) {
|
||||
let byte2 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte2);
|
||||
byte2 = byte2 & 0x3F;
|
||||
|
||||
let byte3 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte3);
|
||||
byte3 = byte3 & 0x3F;
|
||||
|
||||
const encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x06) | byte3;
|
||||
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x0800, Infinity], true);
|
||||
} else if ((byte1 & 0xF8) === 0xF0) {
|
||||
let byte2 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte2);
|
||||
byte2 = byte2 & 0x3F;
|
||||
|
||||
let byte3 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte3);
|
||||
byte3 = byte3 & 0x3F;
|
||||
|
||||
let byte4 = data[++i] & 0xFF;
|
||||
validateUtf8EncodedCodePoint(byte4);
|
||||
byte4 = byte4 & 0x3F;
|
||||
|
||||
const encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x0C) | (byte3 << 0x06) | byte4;
|
||||
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x010000, 0x10FFFF]);
|
||||
} else {
|
||||
validateUtf8EncodedCodePoint(byte1);
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
};
|
||||
120
packages/core/src/uuid.ts
Normal file
120
packages/core/src/uuid.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Represents a UUID as defined by rfc4122.
|
||||
*/
|
||||
export interface UUID {
|
||||
|
||||
/**
|
||||
* @returns the canonical representation in sets of hexadecimal numbers separated by dashes.
|
||||
*/
|
||||
asHex(): string;
|
||||
|
||||
equals(other: UUID): boolean;
|
||||
}
|
||||
|
||||
class ValueUUID implements UUID {
|
||||
|
||||
constructor(public _value: string) {
|
||||
// empty
|
||||
}
|
||||
|
||||
public asHex(): string {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public equals(other: UUID): boolean {
|
||||
return this.asHex() === other.asHex();
|
||||
}
|
||||
}
|
||||
|
||||
class V4UUID extends ValueUUID {
|
||||
|
||||
private static _chars = ['0', '1', '2', '3', '4', '5', '6', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
|
||||
private static _timeHighBits = ['8', '9', 'a', 'b'];
|
||||
|
||||
private static _oneOf(array: string[]): string {
|
||||
return array[Math.floor(array.length * Math.random())];
|
||||
}
|
||||
|
||||
private static _randomHex(): string {
|
||||
return V4UUID._oneOf(V4UUID._chars);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super([
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
'-',
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
'-',
|
||||
'4',
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
'-',
|
||||
V4UUID._oneOf(V4UUID._timeHighBits),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
'-',
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
V4UUID._randomHex(),
|
||||
].join(''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty UUID that contains only zeros.
|
||||
*/
|
||||
export const empty: UUID = new ValueUUID('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
export function v4(): UUID {
|
||||
return new V4UUID();
|
||||
}
|
||||
|
||||
const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function isUUID(value: string): boolean {
|
||||
return _UUIDPattern.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a UUID that is of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
||||
* @param value A uuid string.
|
||||
*/
|
||||
export function parse(value: string): UUID {
|
||||
if (!isUUID(value)) {
|
||||
throw new Error('invalid uuid');
|
||||
}
|
||||
|
||||
return new ValueUUID(value);
|
||||
}
|
||||
|
||||
export function generateUuid(): string {
|
||||
return v4().asHex();
|
||||
}
|
||||
13
packages/core/tsconfig.json
Normal file
13
packages/core/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../typescript-config/base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"files": ["src/index.ts"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declarationDir": "./dist",
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
|
||||
"preserveConstEnums": true
|
||||
},
|
||||
}
|
||||
12
packages/core/tsup.config.ts
Normal file
12
packages/core/tsup.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig((options) => ({
|
||||
entryPoints: [
|
||||
"src/*.ts"
|
||||
],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
...options,
|
||||
bundle: false
|
||||
}));
|
||||
3
packages/eslint-config/README.md
Normal file
3
packages/eslint-config/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@turbo/eslint-config`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
35
packages/eslint-config/library.js
Normal file
35
packages/eslint-config/library.js
Normal file
@ -0,0 +1,35 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use with
|
||||
* typescript packages.
|
||||
*
|
||||
* This config extends the Vercel Engineering Style Guide.
|
||||
* For more information, see https://github.com/vercel/style-guide
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"@vercel/style-guide/eslint/node",
|
||||
"@vercel/style-guide/eslint/typescript",
|
||||
].map(require.resolve),
|
||||
parserOptions: {
|
||||
project,
|
||||
},
|
||||
plugins: ["only-warn"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["node_modules/", "dist/"],
|
||||
};
|
||||
17
packages/eslint-config/package.json
Normal file
17
packages/eslint-config/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"library.js",
|
||||
"react.js",
|
||||
"storybook.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vercel/style-guide": "^5.2.0",
|
||||
"eslint-config-turbo": "^2.0.0",
|
||||
"eslint-plugin-mdx": "^3.1.5",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-storybook": "^0.8.0"
|
||||
}
|
||||
}
|
||||
39
packages/eslint-config/react.js
vendored
Normal file
39
packages/eslint-config/react.js
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use a library
|
||||
* that utilizes React.
|
||||
*
|
||||
* This config extends the Vercel Engineering Style Guide.
|
||||
* For more information, see https://github.com/vercel/style-guide
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"@vercel/style-guide/eslint/browser",
|
||||
"@vercel/style-guide/eslint/typescript",
|
||||
"@vercel/style-guide/eslint/react",
|
||||
].map(require.resolve),
|
||||
parserOptions: {
|
||||
project,
|
||||
},
|
||||
plugins: ["only-warn"],
|
||||
globals: {
|
||||
JSX: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js", "**/*.css"],
|
||||
// add rules configurations here
|
||||
rules: {
|
||||
"import/no-default-export": "off",
|
||||
},
|
||||
};
|
||||
45
packages/eslint-config/storybook.js
Normal file
45
packages/eslint-config/storybook.js
Normal file
@ -0,0 +1,45 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use with
|
||||
* typescript packages.
|
||||
*
|
||||
* This config extends the Vercel Engineering Style Guide.
|
||||
* For more information, see https://github.com/vercel/style-guide
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:mdx/recommended",
|
||||
...[
|
||||
"@vercel/style-guide/eslint/node",
|
||||
"@vercel/style-guide/eslint/typescript",
|
||||
"@vercel/style-guide/eslint/browser",
|
||||
"@vercel/style-guide/eslint/react",
|
||||
].map(require.resolve),
|
||||
],
|
||||
parserOptions: {
|
||||
project,
|
||||
},
|
||||
plugins: ["only-warn"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["node_modules/", "dist/"],
|
||||
// add rules configurations here
|
||||
rules: {
|
||||
"import/no-default-export": "off",
|
||||
},
|
||||
};
|
||||
12
packages/fs/.editorconfig
Normal file
12
packages/fs/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user