feat: init docs by vitepress
This commit is contained in:
parent
a7820908d7
commit
9e08c2a224
|
@ -103,4 +103,5 @@ typings/
|
|||
codealike.json
|
||||
.node
|
||||
|
||||
.must.config.js
|
||||
# docs
|
||||
.vitepress
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"jsxSingleQuote": false,
|
||||
|
|
4
TODOS.md
4
TODOS.md
|
@ -12,3 +12,7 @@
|
|||
7. github workflows
|
||||
|
||||
lodaes replace
|
||||
|
||||
IDE 的引擎设计
|
||||
工作区的虚拟文件设计
|
||||
窗口、视图、编辑器、边栏、面板的设计
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
|
@ -0,0 +1,8 @@
|
|||
# 你好,世界
|
||||
|
||||
step1: 启动引擎,新建一个项目,新建一个低代码文件(.lc)
|
||||
step2: 打开画布
|
||||
step3: 拖拽组件
|
||||
step4: 点击保存 (cmd + s)
|
||||
step5: 点击预览 (cmd + p)
|
||||
step6: 点击关闭,重新打开这个页面,内容不变
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: 'LowCodeEngine'
|
||||
text: '基于 LowCodeEngine 快速打造高生产力的低代码研发平台'
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 介绍
|
||||
link: /guide/introduction
|
||||
- theme: alt
|
||||
text: 快速开始
|
||||
link: /guide/quick-start
|
||||
|
||||
features:
|
||||
- title: 标准化协议
|
||||
details: 底层协议栈定义的是标准,标准的统一让上层产物的互通成为可能
|
||||
- title: 最小内核
|
||||
details: 精心打造低代码领域的编排、入料、出码、渲染模块
|
||||
- title: 最强生态
|
||||
details: 配套生态开箱即用,打造企业级低代码技术体系
|
||||
---
|
|
@ -0,0 +1,85 @@
|
|||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````md
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@alilc/lowcode-engine-docs",
|
||||
"version": "2.0.0-alpha.0",
|
||||
"description": "低代码引擎版本化文档",
|
||||
"keywords": [
|
||||
"lowcode",
|
||||
"engine",
|
||||
"docs"
|
||||
],
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev .",
|
||||
"docs:build": "vitepress build .",
|
||||
"docs:preview": "vitepress preview ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "http",
|
||||
"url": "https://github.com/alibaba/lowcode-engine/tree/main"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.3.1"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.3 KiB |
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
type Event,
|
||||
type EventDisposable,
|
||||
type EventListener,
|
||||
Emitter,
|
||||
Events,
|
||||
LinkedList,
|
||||
TypeConstraint,
|
||||
validateConstraints,
|
||||
Iterable,
|
||||
IDisposable,
|
||||
Disposable,
|
||||
toDisposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { ICommand, ICommandHandler } from './command';
|
||||
import { Extensions, Registry } from '../extension/registry';
|
||||
|
@ -15,27 +15,24 @@ import { ICommandService } from './commandService';
|
|||
export type ICommandsMap = Map<string, ICommand>;
|
||||
|
||||
export interface ICommandRegistry {
|
||||
onDidRegisterCommand: Event<string>;
|
||||
onDidRegisterCommand: Events.Event<string>;
|
||||
|
||||
registerCommand(id: string, command: ICommandHandler): EventDisposable;
|
||||
registerCommand(command: ICommand): EventDisposable;
|
||||
registerCommand(id: string, command: ICommandHandler): IDisposable;
|
||||
registerCommand(command: ICommand): IDisposable;
|
||||
|
||||
registerCommandAlias(oldId: string, newId: string): EventDisposable;
|
||||
registerCommandAlias(oldId: string, newId: string): IDisposable;
|
||||
|
||||
getCommand(id: string): ICommand | undefined;
|
||||
getCommands(): ICommandsMap;
|
||||
}
|
||||
|
||||
class CommandsRegistryImpl implements ICommandRegistry {
|
||||
class CommandsRegistryImpl extends Disposable implements ICommandRegistry {
|
||||
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
||||
|
||||
private readonly _didRegisterCommandEmitter = new Emitter<string>();
|
||||
private readonly _onDidRegisterCommand = this._addDispose(new Events.Emitter<string>());
|
||||
onDidRegisterCommand = this._onDidRegisterCommand.event;
|
||||
|
||||
onDidRegisterCommand(fn: EventListener<string>) {
|
||||
return this._didRegisterCommandEmitter.on(fn);
|
||||
}
|
||||
|
||||
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable {
|
||||
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): IDisposable {
|
||||
if (!idOrCommand) {
|
||||
throw new Error(`invalid command`);
|
||||
}
|
||||
|
@ -71,21 +68,21 @@ class CommandsRegistryImpl implements ICommandRegistry {
|
|||
|
||||
const removeFn = commands.unshift(idOrCommand);
|
||||
|
||||
const ret = () => {
|
||||
const ret = toDisposable(() => {
|
||||
removeFn();
|
||||
const command = this._commands.get(id);
|
||||
if (command?.isEmpty()) {
|
||||
this._commands.delete(id);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// tell the world about this command
|
||||
this._didRegisterCommandEmitter.emit(id);
|
||||
this._onDidRegisterCommand.notify(id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
registerCommandAlias(oldId: string, newId: string): EventDisposable {
|
||||
registerCommandAlias(oldId: string, newId: string): IDisposable {
|
||||
return this.registerCommand(oldId, (accessor, ...args) =>
|
||||
accessor.get(ICommandService).executeCommand(newId, ...args),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
// 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 `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* The (no-break space) character.
|
||||
* Unicode Character 'NO-BREAK SPACE' (U+00A0)
|
||||
*/
|
||||
NoBreakSpace = 160,
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
export interface IRelativePattern {
|
||||
/**
|
||||
* A base file path to which this pattern will be matched against relatively.
|
||||
*/
|
||||
readonly base: string;
|
||||
|
||||
/**
|
||||
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
|
||||
* relative to the base path.
|
||||
*
|
||||
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
|
||||
* the file glob pattern will match on `index.js`.
|
||||
*/
|
||||
readonly pattern: string;
|
||||
}
|
||||
|
||||
export interface IExpression {
|
||||
[pattern: string]: boolean | SiblingClause;
|
||||
}
|
||||
|
||||
interface SiblingClause {
|
||||
when: string;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as Schemas from './schemas';
|
||||
|
||||
export { Schemas };
|
||||
|
||||
export * from './charCode';
|
||||
export * from './glob';
|
||||
export * from './keyCodes';
|
||||
export * from './path';
|
||||
export * from './strings';
|
||||
export * from './ternarySearchTree';
|
||||
export * from './uri';
|
|
@ -1243,50 +1243,21 @@ for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
|
|||
IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter;
|
||||
})();
|
||||
|
||||
export namespace KeyCodeUtils {
|
||||
export function toString(keyCode: KeyCode): string {
|
||||
return uiMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function fromString(key: string): KeyCode {
|
||||
return uiMap.strToKeyCode(key);
|
||||
}
|
||||
export function keyCodeToString(keyCode: KeyCode): string {
|
||||
return uiMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function keyCodeFromString(key: string): KeyCode {
|
||||
return uiMap.strToKeyCode(key);
|
||||
}
|
||||
|
||||
export function toUserSettingsUS(keyCode: KeyCode): string {
|
||||
return userSettingsUSMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function toUserSettingsGeneral(keyCode: KeyCode): string {
|
||||
return userSettingsGeneralMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function fromUserSettings(key: string): KeyCode {
|
||||
return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key);
|
||||
}
|
||||
|
||||
export function toElectronAccelerator(keyCode: KeyCode): string | null {
|
||||
if (keyCode >= KeyCode.Numpad0 && keyCode <= KeyCode.NumpadDivide) {
|
||||
// [Electron Accelerators] Electron is able to parse numpad keys, but unfortunately it
|
||||
// renders them just as regular keys in menus. For example, num0 is rendered as "0",
|
||||
// numdiv is rendered as "/", numsub is rendered as "-".
|
||||
//
|
||||
// This can lead to incredible confusion, as it makes numpad based keybindings indistinguishable
|
||||
// from keybindings based on regular keys.
|
||||
//
|
||||
// We therefore need to fall back to custom rendering for numpad keys.
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyCode.UpArrow:
|
||||
return 'Up';
|
||||
case KeyCode.DownArrow:
|
||||
return 'Down';
|
||||
case KeyCode.LeftArrow:
|
||||
return 'Left';
|
||||
case KeyCode.RightArrow:
|
||||
return 'Right';
|
||||
}
|
||||
|
||||
return uiMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function keyCodeToUserSettingsUS(keyCode: KeyCode): string {
|
||||
return userSettingsUSMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function keyCodeToUserSettingsGeneral(keyCode: KeyCode): string {
|
||||
return userSettingsGeneralMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function keyCodeFromUserSettings(key: string): KeyCode {
|
||||
return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key);
|
||||
}
|
||||
|
||||
export const enum KeyMod {
|
|
@ -0,0 +1,279 @@
|
|||
import { CharCode } from './charCode';
|
||||
|
||||
export function normalize(path: string): string {
|
||||
if (path.length === 0) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
const isAbsolute = path.charCodeAt(0) === CharCode.Slash;
|
||||
const trailingSeparator = path.charCodeAt(path.length - 1) === CharCode.Slash;
|
||||
|
||||
// Normalize the path
|
||||
path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
|
||||
|
||||
if (path.length === 0) {
|
||||
if (isAbsolute) {
|
||||
return '/';
|
||||
}
|
||||
return trailingSeparator ? './' : '.';
|
||||
}
|
||||
if (trailingSeparator) {
|
||||
path += '/';
|
||||
}
|
||||
|
||||
return isAbsolute ? `/${path}` : path;
|
||||
}
|
||||
|
||||
export function join(...paths: string[]): string {
|
||||
if (paths.length === 0) {
|
||||
return '.';
|
||||
}
|
||||
let joined;
|
||||
for (let i = 0; i < paths.length; ++i) {
|
||||
const arg = paths[i];
|
||||
if (arg.length > 0) {
|
||||
if (joined === undefined) {
|
||||
joined = arg;
|
||||
} else {
|
||||
joined += `/${arg}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (joined === undefined) {
|
||||
return '.';
|
||||
}
|
||||
return normalize(joined);
|
||||
}
|
||||
|
||||
export function dirname(path: string): string {
|
||||
if (path.length === 0) {
|
||||
return '.';
|
||||
}
|
||||
const hasRoot = path.charCodeAt(0) === CharCode.Slash;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
for (let i = path.length - 1; i >= 1; --i) {
|
||||
if (path.charCodeAt(i) === CharCode.Slash) {
|
||||
if (!matchedSlash) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// We saw the first non-path separator
|
||||
matchedSlash = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) {
|
||||
return hasRoot ? '/' : '.';
|
||||
}
|
||||
if (hasRoot && end === 1) {
|
||||
return '//';
|
||||
}
|
||||
return path.slice(0, end);
|
||||
}
|
||||
|
||||
export function basename(path: string, suffix?: string): string {
|
||||
let start = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
let i;
|
||||
|
||||
if (suffix !== undefined && suffix.length > 0 && suffix.length <= path.length) {
|
||||
if (suffix === path) {
|
||||
return '';
|
||||
}
|
||||
let extIdx = suffix.length - 1;
|
||||
let firstNonSlashEnd = -1;
|
||||
for (i = path.length - 1; i >= 0; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (code === CharCode.Slash) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (firstNonSlashEnd === -1) {
|
||||
// We saw the first non-path separator, remember this index in case
|
||||
// we need it if the extension ends up not matching
|
||||
matchedSlash = false;
|
||||
firstNonSlashEnd = i + 1;
|
||||
}
|
||||
if (extIdx >= 0) {
|
||||
// Try to match the explicit extension
|
||||
if (code === suffix.charCodeAt(extIdx)) {
|
||||
if (--extIdx === -1) {
|
||||
// We matched the extension, so mark this as the end of our path
|
||||
// component
|
||||
end = i;
|
||||
}
|
||||
} else {
|
||||
// Extension does not match, so our result is the entire path
|
||||
// component
|
||||
extIdx = -1;
|
||||
end = firstNonSlashEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end) {
|
||||
end = firstNonSlashEnd;
|
||||
} else if (end === -1) {
|
||||
end = path.length;
|
||||
}
|
||||
return path.slice(start, end);
|
||||
}
|
||||
for (i = path.length - 1; i >= 0; --i) {
|
||||
if (path.charCodeAt(i) === CharCode.Slash) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// path component
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) {
|
||||
return '';
|
||||
}
|
||||
return path.slice(start, end);
|
||||
}
|
||||
|
||||
export function extname(path: string): string {
|
||||
let startDot = -1;
|
||||
let startPart = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (code === CharCode.Slash) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CharCode.Period) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) {
|
||||
startDot = i;
|
||||
} else if (preDotState !== 1) {
|
||||
preDotState = 1;
|
||||
}
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
|
||||
function isPosixPathSeparator(code: number | undefined) {
|
||||
return code === CharCode.Slash;
|
||||
}
|
||||
|
||||
// Resolves . and .. elements in a path with directory names
|
||||
function normalizeString(
|
||||
path: string,
|
||||
allowAboveRoot: boolean,
|
||||
separator: string,
|
||||
isPathSeparator: (code?: number) => boolean,
|
||||
) {
|
||||
let res = '';
|
||||
let lastSegmentLength = 0;
|
||||
let lastSlash = -1;
|
||||
let dots = 0;
|
||||
let code = 0;
|
||||
for (let i = 0; i <= path.length; ++i) {
|
||||
if (i < path.length) {
|
||||
code = path.charCodeAt(i);
|
||||
} else if (isPathSeparator(code)) {
|
||||
break;
|
||||
} else {
|
||||
code = CharCode.Slash;
|
||||
}
|
||||
|
||||
if (isPathSeparator(code)) {
|
||||
if (lastSlash === i - 1 || dots === 1) {
|
||||
// NOOP
|
||||
} else if (dots === 2) {
|
||||
if (
|
||||
res.length < 2 ||
|
||||
lastSegmentLength !== 2 ||
|
||||
res.charCodeAt(res.length - 1) !== CharCode.Period ||
|
||||
res.charCodeAt(res.length - 2) !== CharCode.Period
|
||||
) {
|
||||
if (res.length > 2) {
|
||||
const lastSlashIndex = res.lastIndexOf(separator);
|
||||
if (lastSlashIndex === -1) {
|
||||
res = '';
|
||||
lastSegmentLength = 0;
|
||||
} else {
|
||||
res = res.slice(0, lastSlashIndex);
|
||||
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
} else if (res.length !== 0) {
|
||||
res = '';
|
||||
lastSegmentLength = 0;
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
res += res.length > 0 ? `${separator}..` : '..';
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0) {
|
||||
res += `${separator}${path.slice(lastSlash + 1, i)}`;
|
||||
} else {
|
||||
res = path.slice(lastSlash + 1, i);
|
||||
}
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
} else if (code === CharCode.Period && dots !== -1) {
|
||||
++dots;
|
||||
} else {
|
||||
dots = -1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export const http = 'http';
|
||||
|
||||
export const https = 'https';
|
||||
|
||||
export const file = 'file';
|
||||
|
||||
export const untitled = 'untitled';
|
|
@ -0,0 +1,88 @@
|
|||
import { CharCode } from './charCode';
|
||||
|
||||
export function compareSubstring(
|
||||
a: string,
|
||||
b: string,
|
||||
aStart: number = 0,
|
||||
aEnd: number = a.length,
|
||||
bStart: number = 0,
|
||||
bEnd: number = b.length,
|
||||
): number {
|
||||
for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) {
|
||||
const codeA = a.charCodeAt(aStart);
|
||||
const codeB = b.charCodeAt(bStart);
|
||||
if (codeA < codeB) {
|
||||
return -1;
|
||||
} else if (codeA > codeB) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const aLen = aEnd - aStart;
|
||||
const bLen = bEnd - bStart;
|
||||
if (aLen < bLen) {
|
||||
return -1;
|
||||
} else if (aLen > bLen) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function compareIgnoreCase(a: string, b: string): number {
|
||||
return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length);
|
||||
}
|
||||
|
||||
export function compareSubstringIgnoreCase(
|
||||
a: string,
|
||||
b: string,
|
||||
aStart: number = 0,
|
||||
aEnd: number = a.length,
|
||||
bStart: number = 0,
|
||||
bEnd: number = b.length,
|
||||
): number {
|
||||
for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) {
|
||||
let codeA = a.charCodeAt(aStart);
|
||||
let codeB = b.charCodeAt(bStart);
|
||||
|
||||
if (codeA === codeB) {
|
||||
// equal
|
||||
continue;
|
||||
}
|
||||
|
||||
if (codeA >= 128 || codeB >= 128) {
|
||||
// not ASCII letters -> fallback to lower-casing strings
|
||||
return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd);
|
||||
}
|
||||
|
||||
// mapper lower-case ascii letter onto upper-case varinats
|
||||
// [97-122] (lower ascii) --> [65-90] (upper ascii)
|
||||
if (isLowerAsciiLetter(codeA)) {
|
||||
codeA -= 32;
|
||||
}
|
||||
if (isLowerAsciiLetter(codeB)) {
|
||||
codeB -= 32;
|
||||
}
|
||||
|
||||
// compare both code points
|
||||
const diff = codeA - codeB;
|
||||
if (diff === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
const aLen = aEnd - aStart;
|
||||
const bLen = bEnd - bStart;
|
||||
|
||||
if (aLen < bLen) {
|
||||
return -1;
|
||||
} else if (aLen > bLen) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isLowerAsciiLetter(code: number): boolean {
|
||||
return code >= CharCode.a && code <= CharCode.z;
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
import { CharCode } from './charCode';
|
||||
import { compareSubstring, compareSubstringIgnoreCase } from './strings';
|
||||
|
||||
export interface IKeyIterator<K> {
|
||||
reset(key: K): this;
|
||||
next(): this;
|
||||
|
||||
hasNext(): boolean;
|
||||
compare(a: string): number;
|
||||
value(): string;
|
||||
}
|
||||
|
||||
export class PathIterator implements IKeyIterator<string> {
|
||||
private _value!: string;
|
||||
private _valueLen!: number;
|
||||
private _from!: number;
|
||||
private _to!: number;
|
||||
|
||||
constructor(private readonly _caseSensitive: boolean = true) {}
|
||||
|
||||
reset(key: string): this {
|
||||
this._from = 0;
|
||||
this._to = 0;
|
||||
this._value = key;
|
||||
this._valueLen = key.length;
|
||||
|
||||
for (let pos = key.length - 1; pos >= 0; pos--, this._valueLen--) {
|
||||
const ch = this._value.charCodeAt(pos);
|
||||
if (!(ch === CharCode.Slash)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.next();
|
||||
}
|
||||
|
||||
next(): this {
|
||||
this._from = this._to;
|
||||
|
||||
let justSeps = true;
|
||||
for (; this._to < this._valueLen; this._to++) {
|
||||
const ch = this._value.charCodeAt(this._to);
|
||||
if (ch === CharCode.Slash) {
|
||||
if (justSeps) {
|
||||
this._from++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
justSeps = false;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hasNext(): boolean {
|
||||
return this._to < this._valueLen;
|
||||
}
|
||||
|
||||
compare(a: string): number {
|
||||
return this._caseSensitive
|
||||
? compareSubstring(a, this._value, 0, a.length, this._from, this._to)
|
||||
: compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to);
|
||||
}
|
||||
|
||||
value(): string {
|
||||
return this._value.substring(this._from, this._to);
|
||||
}
|
||||
}
|
||||
|
||||
class TernarySearchTreeNode<K, V> {
|
||||
height: number = 1;
|
||||
segment!: string;
|
||||
value: V | undefined;
|
||||
key: K | undefined;
|
||||
left: TernarySearchTreeNode<K, V> | undefined;
|
||||
mid: TernarySearchTreeNode<K, V> | undefined;
|
||||
right: TernarySearchTreeNode<K, V> | undefined;
|
||||
|
||||
isEmpty(): boolean {
|
||||
return !this.left && !this.mid && !this.right && !this.value;
|
||||
}
|
||||
|
||||
rotateLeft() {
|
||||
const tmp = this.right!;
|
||||
this.right = tmp.left;
|
||||
tmp.left = this;
|
||||
this.updateHeight();
|
||||
tmp.updateHeight();
|
||||
return tmp;
|
||||
}
|
||||
|
||||
rotateRight() {
|
||||
const tmp = this.left!;
|
||||
this.left = tmp.right;
|
||||
tmp.right = this;
|
||||
this.updateHeight();
|
||||
tmp.updateHeight();
|
||||
return tmp;
|
||||
}
|
||||
|
||||
updateHeight() {
|
||||
this.height = 1 + Math.max(this.heightLeft, this.heightRight);
|
||||
}
|
||||
|
||||
balanceFactor() {
|
||||
return this.heightRight - this.heightLeft;
|
||||
}
|
||||
|
||||
get heightLeft() {
|
||||
return this.left?.height ?? 0;
|
||||
}
|
||||
|
||||
get heightRight() {
|
||||
return this.right?.height ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
const enum Dir {
|
||||
Left = -1,
|
||||
Mid = 0,
|
||||
Right = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* TST的应用场景包括但不限于搜索引擎、代码检查、自动补全等功能,尤其适用于大量字符串查询的场景 。
|
||||
* 例如,在实现搜索框智能提示功能时,TST可以高效地进行前缀匹配,快速给出用户可能的输入选项 。
|
||||
*/
|
||||
export class TernarySearchTree<K, V> {
|
||||
static forPaths<E>(ignorePathCasing = false): TernarySearchTree<string, E> {
|
||||
return new TernarySearchTree<string, E>(new PathIterator(ignorePathCasing));
|
||||
}
|
||||
|
||||
private _iter: IKeyIterator<K>;
|
||||
|
||||
private _root: TernarySearchTreeNode<K, V> | undefined;
|
||||
|
||||
constructor(segments: IKeyIterator<K>) {
|
||||
this._iter = segments;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._root = undefined;
|
||||
}
|
||||
|
||||
set(key: K, element: V): V | undefined {
|
||||
const iter = this._iter.reset(key);
|
||||
|
||||
if (!this._root) {
|
||||
this._root = new TernarySearchTreeNode<K, V>();
|
||||
this._root.segment = iter.value();
|
||||
}
|
||||
|
||||
const stack: [Dir, TernarySearchTreeNode<K, V>][] = [];
|
||||
|
||||
let node: TernarySearchTreeNode<K, V> = this._root;
|
||||
while (true) {
|
||||
const val = iter.compare(node.segment);
|
||||
|
||||
if (val > 0) {
|
||||
// left
|
||||
if (!node.left) {
|
||||
node.left = new TernarySearchTreeNode<K, V>();
|
||||
node.left.segment = iter.value();
|
||||
}
|
||||
stack.push([Dir.Left, node]);
|
||||
node = node.left;
|
||||
} else if (val < 0) {
|
||||
// right
|
||||
if (!node.right) {
|
||||
node.right = new TernarySearchTreeNode<K, V>();
|
||||
node.right.segment = iter.value();
|
||||
}
|
||||
stack.push([Dir.Right, node]);
|
||||
node = node.right;
|
||||
} else if (iter.hasNext()) {
|
||||
// mid
|
||||
iter.next();
|
||||
if (!node.mid) {
|
||||
node.mid = new TernarySearchTreeNode<K, V>();
|
||||
node.mid.segment = iter.value();
|
||||
}
|
||||
stack.push([Dir.Mid, node]);
|
||||
node = node.mid;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set value
|
||||
const oldElement = node.value;
|
||||
node.value = element;
|
||||
node.key = key;
|
||||
|
||||
// balance
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
const node = stack[i][1];
|
||||
|
||||
node.updateHeight();
|
||||
const bf = node.balanceFactor();
|
||||
|
||||
if (bf < -1 || bf > 1) {
|
||||
// needs rotate
|
||||
const d1 = stack[i][0];
|
||||
const d2 = stack[i + 1][0];
|
||||
|
||||
if (d1 === Dir.Right && d2 === Dir.Right) {
|
||||
//right, right -> rotate left
|
||||
stack[i][1] = node.rotateLeft();
|
||||
} else if (d1 === Dir.Left && d2 === Dir.Left) {
|
||||
// left, left -> rotate right
|
||||
stack[i][1] = node.rotateRight();
|
||||
} else if (d1 === Dir.Right && d2 === Dir.Left) {
|
||||
// right, left -> double rotate right, left
|
||||
node.right = stack[i + 1][1] = stack[i + 1][1].rotateRight();
|
||||
stack[i][1] = node.rotateLeft();
|
||||
} else if (d1 === Dir.Left && d2 === Dir.Right) {
|
||||
// left, right -> double rotate left, right
|
||||
node.left = stack[i + 1][1] = stack[i + 1][1].rotateLeft();
|
||||
stack[i][1] = node.rotateRight();
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// patch path to parent
|
||||
if (i > 0) {
|
||||
switch (stack[i - 1][0]) {
|
||||
case Dir.Left:
|
||||
stack[i - 1][1].left = stack[i][1];
|
||||
break;
|
||||
case Dir.Right:
|
||||
stack[i - 1][1].right = stack[i][1];
|
||||
break;
|
||||
case Dir.Mid:
|
||||
stack[i - 1][1].mid = stack[i][1];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this._root = stack[0][1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return oldElement;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this._getNode(key)?.value;
|
||||
}
|
||||
|
||||
private _getNode(key: K) {
|
||||
const iter = this._iter.reset(key);
|
||||
let node = this._root;
|
||||
while (node) {
|
||||
const val = iter.compare(node.segment);
|
||||
if (val > 0) {
|
||||
// left
|
||||
node = node.left;
|
||||
} else if (val < 0) {
|
||||
// right
|
||||
node = node.right;
|
||||
} else if (iter.hasNext()) {
|
||||
// mid
|
||||
iter.next();
|
||||
node = node.mid;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
findSubstr(key: K): V | undefined {
|
||||
const iter = this._iter.reset(key);
|
||||
|
||||
let node = this._root;
|
||||
let candidate: V | undefined = undefined;
|
||||
while (node) {
|
||||
const val = iter.compare(node.segment);
|
||||
if (val > 0) {
|
||||
node = node.left;
|
||||
} else if (val < 0) {
|
||||
node = node.right;
|
||||
} else if (iter.hasNext()) {
|
||||
iter.next();
|
||||
|
||||
candidate = node.value || candidate;
|
||||
node = node.mid;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return node?.value ?? candidate;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,133 @@
|
|||
import * as Schemas from './schemas';
|
||||
import { join } from './path';
|
||||
|
||||
export interface UriComponents {
|
||||
path: string;
|
||||
scheme?: string;
|
||||
authority?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
const EMPTY = '';
|
||||
const SLASH = '/';
|
||||
const URI_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 {
|
||||
readonly scheme: string;
|
||||
readonly authority: string;
|
||||
readonly path: string;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
constructor(scheme?: string, authority?: string, path?: string);
|
||||
constructor(data?: UriComponents);
|
||||
constructor(data?: UriComponents | string, authority?: string, path?: string) {
|
||||
if (typeof data === 'object') {
|
||||
this.scheme = data.scheme || EMPTY;
|
||||
this.authority = data.authority || EMPTY;
|
||||
this.path = data.path || EMPTY;
|
||||
} else {
|
||||
this.scheme = data || 'file';
|
||||
this.authority = authority || EMPTY;
|
||||
this.path = path || EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
with(change: { scheme?: string; path?: string }): URI {
|
||||
let { scheme, path } = change;
|
||||
|
||||
if (scheme === undefined) {
|
||||
scheme = this.scheme;
|
||||
} else if (scheme === null) {
|
||||
scheme = EMPTY;
|
||||
}
|
||||
|
||||
if (path === undefined) {
|
||||
path = this.path;
|
||||
} else if (path === null) {
|
||||
path = EMPTY;
|
||||
}
|
||||
|
||||
return new URI({ scheme, path });
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
let res = '';
|
||||
const { scheme, authority, path } = this;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
res += ':';
|
||||
}
|
||||
if (authority || scheme === Schemas.file) {
|
||||
res += SLASH;
|
||||
res += SLASH;
|
||||
}
|
||||
if (authority) {
|
||||
res += authority.toLowerCase();
|
||||
}
|
||||
if (path) {
|
||||
res += path;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (thing instanceof URI) return true;
|
||||
if (!thing) return false;
|
||||
|
||||
return typeof thing.path === 'string' && typeof thing.authority === 'string' && typeof thing.scheme === 'string';
|
||||
}
|
||||
|
||||
static joinPath(uri: URI, ...pathSegments: string[]) {
|
||||
if (!uri.path) {
|
||||
throw new URIError(`cannot call joinPath on URI without path`);
|
||||
}
|
||||
|
||||
return uri.with({ path: join(uri.path, ...pathSegments) });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): URI {
|
||||
return new URI(components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new URI from a string, e.g. `/some/path`
|
||||
*
|
||||
* @param value A string which represents an URI (see `URI#toString`).
|
||||
*/
|
||||
static parse(value: string): URI {
|
||||
const match = URI_REGEXP.exec(value);
|
||||
if (!match) {
|
||||
return new URI();
|
||||
}
|
||||
return new URI(match[2] || EMPTY, match[4] || EMPTY, match[5] || EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
class URIError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`[UriError]: ${message}`);
|
||||
this.name = 'URIError';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
type Event,
|
||||
Emitter,
|
||||
Events,
|
||||
type StringDictionary,
|
||||
type JSONSchemaType,
|
||||
jsonTypes,
|
||||
IJSONSchema,
|
||||
types,
|
||||
Disposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { isUndefined, isObject } from 'lodash-es';
|
||||
import { Extensions, Registry } from '../extension/registry';
|
||||
|
@ -44,7 +44,7 @@ export interface IConfigurationRegistry {
|
|||
* Event that fires whenever a configuration has been
|
||||
* registered.
|
||||
*/
|
||||
readonly onDidUpdateConfiguration: Event<{
|
||||
readonly onDidUpdateConfiguration: Events.Event<{
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean;
|
||||
}>;
|
||||
|
@ -133,7 +133,15 @@ export const allSettings: {
|
|||
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
||||
} = { properties: {}, patternProperties: {} };
|
||||
|
||||
export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
||||
export class ConfigurationRegistryImpl extends Disposable implements IConfigurationRegistry {
|
||||
private _onDidUpdateConfiguration = this._addDispose(
|
||||
new Events.Emitter<{
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean;
|
||||
}>(),
|
||||
);
|
||||
onDidUpdateConfiguration = this._onDidUpdateConfiguration.event;
|
||||
|
||||
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
||||
private readonly configurationDefaultsOverrides: Map<
|
||||
string,
|
||||
|
@ -147,12 +155,9 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
private readonly excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private overrideIdentifiers = new Set<string>();
|
||||
|
||||
private propertiesChangeEmitter = new Emitter<{
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean;
|
||||
}>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.configurationDefaultsOverrides = new Map();
|
||||
this.configurationProperties = {};
|
||||
this.excludedConfigurationProperties = {};
|
||||
|
@ -169,7 +174,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
const properties = new Set<string>();
|
||||
|
||||
this.doRegisterConfigurations(configurations, validate, properties);
|
||||
this.propertiesChangeEmitter.emit({ properties });
|
||||
this._onDidUpdateConfiguration.notify({ properties });
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
@ -278,7 +283,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
deregisterConfigurations(configurations: IConfigurationNode[]): void {
|
||||
const properties = new Set<string>();
|
||||
this.doDeregisterConfigurations(configurations, properties);
|
||||
this.propertiesChangeEmitter.emit({ properties });
|
||||
this._onDidUpdateConfiguration.notify({ properties });
|
||||
}
|
||||
|
||||
private doDeregisterConfigurations(
|
||||
|
@ -305,7 +310,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
const properties = new Set<string>();
|
||||
|
||||
this.doRegisterDefaultConfigurations(configurationDefaults, properties);
|
||||
this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true });
|
||||
this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true });
|
||||
}
|
||||
|
||||
private doRegisterDefaultConfigurations(
|
||||
|
@ -476,7 +481,7 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
const properties = new Set<string>();
|
||||
this.doDeregisterDefaultConfigurations(defaultConfigurations, properties);
|
||||
|
||||
this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true });
|
||||
this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true });
|
||||
}
|
||||
|
||||
private doDeregisterDefaultConfigurations(
|
||||
|
@ -608,15 +613,6 @@ export class ConfigurationRegistryImpl implements IConfigurationRegistry {
|
|||
return configurationDefaultsOverrides;
|
||||
}
|
||||
|
||||
onDidUpdateConfiguration(
|
||||
fn: (change: {
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean | undefined;
|
||||
}) => void,
|
||||
) {
|
||||
return this.propertiesChangeEmitter.on(fn);
|
||||
}
|
||||
|
||||
private registerJSONConfiguration(configuration: IConfigurationNode) {
|
||||
const register = (configuration: IConfigurationNode) => {
|
||||
const properties = configuration.properties;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared';
|
||||
import { createDecorator, Disposable, Events } from '@alilc/lowcode-shared';
|
||||
import {
|
||||
Configuration,
|
||||
DefaultConfiguration,
|
||||
|
@ -58,19 +58,24 @@ export interface IConfigurationService {
|
|||
memory?: string[];
|
||||
};
|
||||
|
||||
onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
|
||||
onDidChangeConfiguration: Events.Event<IConfigurationChangeEvent>;
|
||||
}
|
||||
|
||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||
|
||||
export class ConfigurationService implements IConfigurationService {
|
||||
export class ConfigurationService extends Disposable implements IConfigurationService {
|
||||
private configuration: Configuration;
|
||||
private readonly defaultConfiguration: DefaultConfiguration;
|
||||
private readonly userConfiguration: UserConfiguration;
|
||||
|
||||
private readonly didChangeEmitter = new Emitter<IConfigurationChangeEvent>();
|
||||
private readonly _onDidChangeConfiguration = this._addDispose(
|
||||
new Events.Emitter<IConfigurationChangeEvent>(),
|
||||
);
|
||||
onDidChangeConfiguration = this._onDidChangeConfiguration.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.defaultConfiguration = new DefaultConfiguration();
|
||||
this.userConfiguration = new UserConfiguration({});
|
||||
this.configuration = new Configuration(
|
||||
|
@ -172,11 +177,7 @@ export class ConfigurationService implements IConfigurationService {
|
|||
{ data: previous },
|
||||
this.configuration,
|
||||
);
|
||||
this.didChangeEmitter.emit(event);
|
||||
}
|
||||
|
||||
onDidChangeConfiguration(listener: EventListener<IConfigurationChangeEvent>) {
|
||||
return this.didChangeEmitter.on(listener);
|
||||
this._onDidChangeConfiguration.notify(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type StringDictionary, Emitter, type EventListener } from '@alilc/lowcode-shared';
|
||||
import { type StringDictionary, Disposable, Events } from '@alilc/lowcode-shared';
|
||||
import {
|
||||
ConfigurationModel,
|
||||
type IConfigurationModel,
|
||||
|
@ -8,7 +8,6 @@ import {
|
|||
import {
|
||||
ConfigurationRegistry,
|
||||
type IConfigurationPropertySchema,
|
||||
type IConfigurationRegistry,
|
||||
type IRegisteredConfigurationPropertySchema,
|
||||
} from './configurationRegistry';
|
||||
import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es';
|
||||
|
@ -23,11 +22,14 @@ export interface IConfigurationOverrides {
|
|||
overrideIdentifier?: string | null;
|
||||
}
|
||||
|
||||
export class DefaultConfiguration {
|
||||
private emitter = new Emitter<{
|
||||
defaults: ConfigurationModel;
|
||||
properties: string[];
|
||||
}>();
|
||||
export class DefaultConfiguration extends Disposable {
|
||||
private _onDidChangeConfiguration = this._addDispose(
|
||||
new Events.Emitter<{
|
||||
defaults: ConfigurationModel;
|
||||
properties: string[];
|
||||
}>(),
|
||||
);
|
||||
onDidChangeConfiguration = this._onDidChangeConfiguration.event;
|
||||
|
||||
private _configurationModel = ConfigurationModel.createEmptyModel();
|
||||
|
||||
|
@ -49,15 +51,9 @@ export class DefaultConfiguration {
|
|||
return this.configurationModel;
|
||||
}
|
||||
|
||||
onDidChangeConfiguration(
|
||||
listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>,
|
||||
) {
|
||||
return this.emitter.on(listener);
|
||||
}
|
||||
|
||||
private onDidUpdateConfiguration(properties: string[]): void {
|
||||
this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties());
|
||||
this.emitter.emit({ defaults: this.configurationModel, properties });
|
||||
this._onDidChangeConfiguration.notify({ defaults: this.configurationModel, properties });
|
||||
}
|
||||
|
||||
private resetConfigurationModel(): void {
|
||||
|
@ -238,7 +234,7 @@ export class UserConfiguration {
|
|||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
try {
|
||||
// const content = await this.fileService.readFile(this.userSettingsResource);
|
||||
// 可能远程请求或者读取对应配置
|
||||
this.parser.parse({}, this.parseOptions);
|
||||
return this.parser.configurationModel;
|
||||
} catch (e) {
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
import { type IDisposable, Events } from '@alilc/lowcode-shared';
|
||||
import { URI, IRelativePattern } from '../common';
|
||||
|
||||
export enum FileType {
|
||||
/**
|
||||
* File is unknown (neither file, directory).
|
||||
*/
|
||||
Unknown = 0,
|
||||
|
||||
/**
|
||||
* File is a normal file.
|
||||
*/
|
||||
File = 1,
|
||||
|
||||
/**
|
||||
* File is a directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
}
|
||||
|
||||
export enum FilePermission {
|
||||
None = 0,
|
||||
|
||||
/**
|
||||
* File is readonly. Components like editors should not
|
||||
* offer to edit the contents.
|
||||
*/
|
||||
Readable = 1,
|
||||
|
||||
Writable = 2,
|
||||
}
|
||||
|
||||
export interface IStat {
|
||||
/**
|
||||
* The file type.
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
*/
|
||||
readonly mtime: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
*/
|
||||
readonly ctime: number;
|
||||
|
||||
/**
|
||||
* The file permissions.
|
||||
*/
|
||||
readonly permission: FilePermission;
|
||||
}
|
||||
|
||||
export interface IBaseFileStat {
|
||||
/**
|
||||
* The unified resource identifier of this file or folder.
|
||||
*/
|
||||
readonly resource: URI;
|
||||
|
||||
/**
|
||||
* The name which is the last segment
|
||||
* of the {{path}}.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
*
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
readonly mtime: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
*/
|
||||
readonly ctime: number;
|
||||
|
||||
readonly permission: FilePermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file resource with meta information and resolved children if any.
|
||||
*/
|
||||
export interface IFileStat extends IBaseFileStat {
|
||||
/**
|
||||
* The resource is a file.
|
||||
*/
|
||||
readonly isFile: boolean;
|
||||
|
||||
/**
|
||||
* The resource is a directory.
|
||||
*/
|
||||
readonly isDirectory: boolean;
|
||||
}
|
||||
|
||||
export interface IFileOverwriteOptions {
|
||||
/**
|
||||
* Set to `true` to overwrite a file if it exists. Will
|
||||
* throw an error otherwise if the file does exist.
|
||||
*/
|
||||
readonly overwrite?: boolean;
|
||||
}
|
||||
|
||||
export interface IFileCreateOptions {
|
||||
/**
|
||||
* Set to `true` to create parent directory when it does not exist. Will
|
||||
* throw an error otherwise if the file does not exist.
|
||||
*/
|
||||
readonly recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface IFileDeleteOptions {
|
||||
/**
|
||||
* Set to `true` to recursively delete any children of the file. This
|
||||
* only applies to folders and can lead to an error unless provided
|
||||
* if the folder is not empty.
|
||||
*/
|
||||
readonly recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface IFileWriteOptions extends IFileCreateOptions, IFileOverwriteOptions {}
|
||||
|
||||
/**
|
||||
* Identifies a single change in a file.
|
||||
*/
|
||||
export interface IFileChange {
|
||||
/**
|
||||
* The type of change that occurred to the file.
|
||||
*/
|
||||
type: FileChangeType;
|
||||
|
||||
/**
|
||||
* The unified resource identifier of the file that changed.
|
||||
*/
|
||||
readonly resource: URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible changes that can occur to a file.
|
||||
*/
|
||||
export const enum FileChangeType {
|
||||
UPDATED = 1 << 1,
|
||||
ADDED = 1 << 2,
|
||||
DELETED = 1 << 3,
|
||||
}
|
||||
|
||||
export interface IWatchOptions {
|
||||
/**
|
||||
* Set to `true` to watch for changes recursively in a folder
|
||||
* and all of its children.
|
||||
*/
|
||||
recursive: boolean;
|
||||
|
||||
/**
|
||||
* A set of glob patterns or paths to exclude from watching.
|
||||
* Paths can be relative or absolute and when relative are
|
||||
* resolved against the watched folder. Glob patterns are
|
||||
* always matched relative to the watched folder.
|
||||
*/
|
||||
excludes: string[];
|
||||
|
||||
/**
|
||||
* An optional set of glob patterns or paths to include for
|
||||
* watching. If not provided, all paths are considered for
|
||||
* events.
|
||||
* Paths can be relative or absolute and when relative are
|
||||
* resolved against the watched folder. Glob patterns are
|
||||
* always matched relative to the watched folder.
|
||||
*/
|
||||
includes?: Array<string | IRelativePattern>;
|
||||
|
||||
/**
|
||||
* If provided, allows to filter the events that the watcher should consider
|
||||
* for emitting. If not provided, all events are emitted.
|
||||
*
|
||||
* For example, to emit added and updated events, set to:
|
||||
* `FileChangeType.ADDED | FileChangeType.UPDATED`.
|
||||
*/
|
||||
filter?: number;
|
||||
}
|
||||
|
||||
export interface IFileSystemWatcher extends IDisposable {
|
||||
/**
|
||||
* An event which fires on file/folder change only for changes
|
||||
* that correlate to the watch request with matching correlation
|
||||
* identifier.
|
||||
*/
|
||||
readonly onDidChange: Events.Event<FileChangesEvent>;
|
||||
}
|
||||
|
||||
export class FileChangesEvent {}
|
||||
|
||||
export enum FsContants {
|
||||
F_OK = 1,
|
||||
R_OK = 1 << 1,
|
||||
W_OK = 1 << 2,
|
||||
}
|
||||
|
||||
export interface IFileSystemProvider {
|
||||
watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher;
|
||||
|
||||
chmod(resource: URI, mode: number): Promise<void>;
|
||||
access(resource: URI, mode?: number): Promise<void>;
|
||||
stat(resource: URI): Promise<IFileStat>;
|
||||
mkdir(resource: URI, opts: IFileWriteOptions): Promise<void>;
|
||||
readdir(resource: URI): Promise<[string, FileType][]>;
|
||||
delete(resource: URI, opts: IFileDeleteOptions): Promise<void>;
|
||||
|
||||
rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
|
||||
// copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
|
||||
|
||||
readFile(resource: URI): Promise<string>;
|
||||
writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export enum FileSystemErrorCode {
|
||||
FileExists = 'EntryExists',
|
||||
FileNotFound = 'EntryNotFound',
|
||||
FileNotADirectory = 'EntryNotADirectory',
|
||||
FileIsADirectory = 'EntryIsADirectory',
|
||||
FileTooLarge = 'EntryTooLarge',
|
||||
FileNotReadable = 'EntryNotReadable',
|
||||
FileNotWritable = 'EntryNotWritable',
|
||||
Unavailable = 'Unavailable',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export class FileSystemError extends Error {
|
||||
static create(error: Error | string, code: FileSystemErrorCode): FileSystemError {
|
||||
const providerError = new FileSystemError(error.toString(), code);
|
||||
return providerError;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
message: string,
|
||||
readonly code: FileSystemErrorCode,
|
||||
) {
|
||||
super(message);
|
||||
this.name = code ? `${code} (FileSystemError)` : `FileSystemError`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { createDecorator, Disposable, IDisposable, toDisposable } from '@alilc/lowcode-shared';
|
||||
import { type IFileSystemProvider } from './file';
|
||||
import { URI } from '../common';
|
||||
|
||||
export interface IFileService {
|
||||
/**
|
||||
* Registers a file system provider for a certain scheme.
|
||||
*/
|
||||
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable;
|
||||
|
||||
/**
|
||||
* Returns a file system provider for a certain scheme.
|
||||
*/
|
||||
getProvider(scheme: string): IFileSystemProvider | undefined;
|
||||
|
||||
/**
|
||||
* Checks if the file service has a registered provider for the
|
||||
* provided resource.
|
||||
*
|
||||
* Note: this does NOT account for contributed providers from
|
||||
* extensions that have not been activated yet. To include those,
|
||||
* consider to call `await fileService.canHandleResource(resource)`.
|
||||
*/
|
||||
hasProvider(resource: URI): boolean;
|
||||
|
||||
withProvider(resource: URI): IFileSystemProvider | undefined;
|
||||
|
||||
/**
|
||||
* Frees up any resources occupied by this service.
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export const IFileService = createDecorator<IFileService>('fileService');
|
||||
|
||||
export class FileService extends Disposable implements IFileService {
|
||||
private readonly provider = new Map<string, IFileSystemProvider>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
|
||||
this.provider.set(scheme, provider);
|
||||
|
||||
return toDisposable(() => {
|
||||
this.provider.delete(scheme);
|
||||
});
|
||||
}
|
||||
|
||||
getProvider(scheme: string): IFileSystemProvider | undefined {
|
||||
return this.provider.get(scheme);
|
||||
}
|
||||
|
||||
hasProvider(resource: URI): boolean {
|
||||
return this.provider.has(resource.scheme);
|
||||
}
|
||||
|
||||
withProvider(resource: URI): IFileSystemProvider | undefined {
|
||||
return this.provider.get(resource.scheme);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,346 @@
|
|||
import { Events } from '@alilc/lowcode-shared';
|
||||
import {
|
||||
FileType,
|
||||
IFileSystemProvider,
|
||||
type IStat,
|
||||
type IFileChange,
|
||||
type IFileDeleteOptions,
|
||||
type IFileSystemWatcher,
|
||||
type IWatchOptions,
|
||||
type IFileWriteOptions,
|
||||
type IFileOverwriteOptions,
|
||||
FileSystemErrorCode,
|
||||
FileSystemError,
|
||||
IFileStat,
|
||||
FilePermission,
|
||||
FileChangeType,
|
||||
FsContants,
|
||||
} from './file';
|
||||
import { URI, basename, dirname } from '../common';
|
||||
|
||||
class File implements IStat {
|
||||
readonly type: FileType.File;
|
||||
readonly ctime: number;
|
||||
mtime: number;
|
||||
|
||||
name: string;
|
||||
data: string;
|
||||
permission = FilePermission.Writable;
|
||||
|
||||
constructor(name: string) {
|
||||
this.type = FileType.File;
|
||||
this.ctime = Date.now();
|
||||
this.mtime = Date.now();
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class Directory implements IStat {
|
||||
readonly type: FileType.Directory;
|
||||
readonly ctime: number;
|
||||
mtime: number;
|
||||
|
||||
name: string;
|
||||
permission = FilePermission.Writable;
|
||||
readonly entries: Map<string, File | Directory>;
|
||||
|
||||
constructor(name: string) {
|
||||
this.type = FileType.Directory;
|
||||
this.ctime = Date.now();
|
||||
this.mtime = Date.now();
|
||||
this.name = name;
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.entries.size;
|
||||
}
|
||||
}
|
||||
|
||||
type Entry = File | Directory;
|
||||
|
||||
export class InMemoryFileSystemProvider implements IFileSystemProvider {
|
||||
private readonly _onDidChangeFile = new Events.Emitter<readonly IFileChange[]>();
|
||||
onDidChangeFile = this._onDidChangeFile.event;
|
||||
|
||||
private _bufferedChanges: IFileChange[] = [];
|
||||
private _fireSoonHandle: number | undefined;
|
||||
|
||||
private readonly _root = new Directory('');
|
||||
|
||||
async chmod(resource: URI, mode: number): Promise<void> {
|
||||
const entry = this._lookup(resource.path, false);
|
||||
|
||||
if (FilePermission.Writable < mode) {
|
||||
throw FileSystemError.create('Unsupported mode', FileSystemErrorCode.Unavailable);
|
||||
}
|
||||
|
||||
entry.permission = mode;
|
||||
}
|
||||
|
||||
async access(resource: URI, mode: number = FsContants.F_OK): Promise<void> {
|
||||
const entry = this._lookup(resource.path, false);
|
||||
|
||||
if (mode === FsContants.F_OK) return;
|
||||
if (mode === FsContants.R_OK && entry.permission >= FilePermission.Readable) {
|
||||
return;
|
||||
}
|
||||
if (mode === FsContants.W_OK && entry.permission >= FilePermission.Writable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IFileStat> {
|
||||
const file = this._lookup(resource.path, false);
|
||||
|
||||
return {
|
||||
resource,
|
||||
name: file.name,
|
||||
ctime: file.ctime,
|
||||
mtime: file.mtime,
|
||||
permission: file.permission,
|
||||
isDirectory: file.type === FileType.Directory,
|
||||
isFile: file.type === FileType.File,
|
||||
};
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher {
|
||||
return { resource, opts } as any as IFileSystemWatcher;
|
||||
}
|
||||
|
||||
async mkdir(resource: URI, opts: IFileWriteOptions): Promise<void> {
|
||||
const base = basename(resource.path);
|
||||
const dir = dirname(resource.path);
|
||||
const parent = this._lookupAsDirectory(dir, true);
|
||||
|
||||
if (parent) {
|
||||
if (!opts.overwrite && parent.entries.has(base)) {
|
||||
throw FileSystemError.create('directory exists', FileSystemErrorCode.FileExists);
|
||||
}
|
||||
|
||||
const entry = new Directory(base);
|
||||
entry.mtime = Date.now();
|
||||
parent.entries.set(entry.name, entry);
|
||||
this._fireSoon(
|
||||
{ resource: resource.with({ path: dir }), type: FileChangeType.UPDATED },
|
||||
{ resource, type: FileChangeType.ADDED },
|
||||
);
|
||||
} else {
|
||||
if (!opts.recursive) {
|
||||
throw FileSystemError.create('parent directory not found', FileSystemErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
this._mkdirRecursive(resource);
|
||||
}
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const dir = this._lookupAsDirectory(resource.path, false);
|
||||
|
||||
if (dir.permission < FilePermission.Readable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotReadable);
|
||||
}
|
||||
|
||||
return [...dir.entries.entries()].map(([name, entry]) => [name, entry.type]);
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<string> {
|
||||
const file = this._lookupAsFile(resource.path, false);
|
||||
|
||||
if (file.permission < FilePermission.Readable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotReadable);
|
||||
}
|
||||
|
||||
return file.data;
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise<void> {
|
||||
const base = basename(resource.path);
|
||||
const dir = dirname(resource.path);
|
||||
const dirUri = resource.with({ path: dir });
|
||||
let parent = this._lookupAsDirectory(dir, true);
|
||||
|
||||
if (!parent) {
|
||||
if (!opts.recursive) {
|
||||
throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound);
|
||||
}
|
||||
parent = await this._mkdirRecursive(dirUri);
|
||||
}
|
||||
|
||||
let entry = parent.entries.get(base);
|
||||
if (entry instanceof Directory) {
|
||||
throw FileSystemError.create('file is directory', FileSystemErrorCode.FileIsADirectory);
|
||||
}
|
||||
if (entry && entry.permission < FilePermission.Writable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
|
||||
}
|
||||
if (entry && !opts.overwrite) {
|
||||
throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists);
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
entry = new File(base);
|
||||
parent.entries.set(base, entry);
|
||||
this._fireSoon({ resource, type: FileChangeType.ADDED });
|
||||
}
|
||||
|
||||
entry.mtime = Date.now();
|
||||
entry.data = content;
|
||||
this._fireSoon({ resource, type: FileChangeType.UPDATED });
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
|
||||
const dir = dirname(resource.path);
|
||||
const base = basename(resource.path);
|
||||
const parent = this._lookupAsDirectory(dir, false);
|
||||
|
||||
if (parent.permission < FilePermission.Writable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
|
||||
}
|
||||
|
||||
if (parent.entries.has(base)) {
|
||||
const entry = parent.entries.get(base)!;
|
||||
|
||||
if (entry.permission < FilePermission.Writable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
|
||||
}
|
||||
|
||||
if (entry instanceof Directory) {
|
||||
if (opts.recursive) {
|
||||
parent.entries.delete(base);
|
||||
parent.mtime = Date.now();
|
||||
} else {
|
||||
throw FileSystemError.create('file is directory', FileSystemErrorCode.FileIsADirectory);
|
||||
}
|
||||
} else {
|
||||
parent.entries.delete(base);
|
||||
parent.mtime = Date.now();
|
||||
}
|
||||
|
||||
this._fireSoon(
|
||||
{ resource, type: FileChangeType.DELETED },
|
||||
{ resource: resource.with({ path: dir }), type: FileChangeType.UPDATED },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
|
||||
if (from.path === to.path) return;
|
||||
|
||||
const entry = this._lookup(from.path, false);
|
||||
|
||||
if (entry.permission < FilePermission.Writable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
|
||||
}
|
||||
if (!opts.overwrite) {
|
||||
throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists);
|
||||
}
|
||||
|
||||
const oldParent = this._lookupAsDirectory(dirname(from.path), false);
|
||||
|
||||
const newParent = this._lookupAsDirectory(dirname(to.path), false);
|
||||
const newName = basename(to.path);
|
||||
|
||||
oldParent.entries.delete(entry.name);
|
||||
entry.name = newName;
|
||||
newParent.entries.set(newName, entry);
|
||||
|
||||
this._fireSoon({ resource: to, type: FileChangeType.ADDED }, { resource: from, type: FileChangeType.DELETED });
|
||||
}
|
||||
|
||||
private async _mkdirRecursive(target: URI): Promise<Directory> {
|
||||
const dir = dirname(target.path);
|
||||
const dirUri = target.with({ path: dir });
|
||||
let parent = this._lookupAsDirectory(dir, false);
|
||||
|
||||
if (!parent) {
|
||||
parent = await this._mkdirRecursive(dirUri);
|
||||
}
|
||||
|
||||
if (parent.permission < FilePermission.Writable) {
|
||||
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
|
||||
}
|
||||
|
||||
const directory = new Directory(basename(target.path));
|
||||
directory.mtime = Date.now();
|
||||
parent.entries.set(directory.name, directory);
|
||||
this._fireSoon(
|
||||
{
|
||||
resource: dirUri,
|
||||
type: FileChangeType.UPDATED,
|
||||
},
|
||||
{
|
||||
resource: target,
|
||||
type: FileChangeType.ADDED,
|
||||
},
|
||||
);
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
// --- lookup
|
||||
|
||||
private _lookup(target: string, silent: false): Entry;
|
||||
private _lookup(target: string, silent: boolean): Entry | undefined;
|
||||
private _lookup(target: string, silent: boolean): Entry | undefined {
|
||||
const parts = target.split('/');
|
||||
|
||||
let entry: Entry = this._root;
|
||||
for (const part of parts) {
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
let child: Entry | undefined;
|
||||
if (entry.type === FileType.Directory) {
|
||||
child = entry.entries.get(part);
|
||||
}
|
||||
if (!child) {
|
||||
if (!silent) {
|
||||
throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
entry = child;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private _lookupAsDirectory(target: string, silent: false): Directory;
|
||||
private _lookupAsDirectory(target: string, silent: boolean): Directory | undefined;
|
||||
private _lookupAsDirectory(target: string, silent: boolean): Directory | undefined {
|
||||
const entry = this._lookup(target, silent);
|
||||
|
||||
if (entry instanceof Directory) {
|
||||
return entry;
|
||||
}
|
||||
if (!silent) {
|
||||
throw FileSystemError.create('directory not found', FileSystemErrorCode.FileNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
private _lookupAsFile(target: string, silent: false): File;
|
||||
private _lookupAsFile(target: string, silent: boolean): File | undefined;
|
||||
private _lookupAsFile(target: string, silent: boolean): File | undefined {
|
||||
const entry = this._lookup(target, silent);
|
||||
if (entry instanceof File) {
|
||||
return entry;
|
||||
}
|
||||
if (!silent) {
|
||||
throw FileSystemError.create('file is a directory', FileSystemErrorCode.FileIsADirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private _fireSoon(...changes: IFileChange[]): void {
|
||||
this._bufferedChanges.push(...changes);
|
||||
|
||||
if (this._fireSoonHandle) {
|
||||
clearTimeout(this._fireSoonHandle);
|
||||
}
|
||||
|
||||
this._fireSoonHandle = window.setTimeout(() => {
|
||||
this._onDidChangeFile.notify(this._bufferedChanges);
|
||||
this._bufferedChanges.length = 0;
|
||||
}, 5);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './file';
|
||||
export * from './fileService';
|
||||
export * from './inMemoryFileSystemProvider';
|
|
@ -2,3 +2,8 @@ export * from './configuration';
|
|||
export * from './extension/extension';
|
||||
export * from './resource';
|
||||
export * from './command';
|
||||
export * from './workspace';
|
||||
export * from './common';
|
||||
|
||||
// test
|
||||
export * from './main';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { illegalArgument, KeyCode, OperatingSystem, ScanCode } from '@alilc/lowcode-shared';
|
||||
import { illegalArgument, OperatingSystem } from '@alilc/lowcode-shared';
|
||||
import { KeyCode, ScanCode } from '../common/keyCodes';
|
||||
|
||||
/**
|
||||
* Binary encoding strategy:
|
||||
|
|
|
@ -1,27 +1,70 @@
|
|||
import { InstantiationService } from '@alilc/lowcode-shared';
|
||||
import { IWorkbenchService } from './workbench';
|
||||
import { InstantiationService, BeanContainer, CtorDescriptor } from '@alilc/lowcode-shared';
|
||||
import { ConfigurationService, IConfigurationService } from './configuration';
|
||||
import { IWorkspaceService, WorkspaceService, toWorkspaceIdentifier } from './workspace';
|
||||
import { IWindowService, WindowService } from './window';
|
||||
import { IFileService, FileService, InMemoryFileSystemProvider } from './file';
|
||||
import { URI } from './common/uri';
|
||||
import * as Schemas from './common/schemas';
|
||||
|
||||
class TestMainApplication {
|
||||
instantiationService: InstantiationService;
|
||||
|
||||
constructor() {
|
||||
console.log('main application');
|
||||
}
|
||||
|
||||
async main() {
|
||||
const workbench = instantiationService.get(IWorkbenchService);
|
||||
await this._initServices();
|
||||
|
||||
await configurationService.initialize();
|
||||
workbench.initialize();
|
||||
const [workspaceService, windowService, fileService] = this.instantiationService.invokeFunction((accessor) => [
|
||||
accessor.get(IWorkspaceService),
|
||||
accessor.get(IWindowService),
|
||||
accessor.get(IFileService),
|
||||
]);
|
||||
|
||||
fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider());
|
||||
|
||||
try {
|
||||
const uri = URI.from({ path: '/Desktop' });
|
||||
|
||||
await workspaceService.enterWorkspace(toWorkspaceIdentifier(uri.path));
|
||||
|
||||
const fileUri = URI.joinPath(uri, 'test.lc');
|
||||
|
||||
await windowService.open({
|
||||
urisToOpen: [{ fileUri }],
|
||||
openOnlyIfExists: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('error', e);
|
||||
}
|
||||
}
|
||||
|
||||
createServices() {
|
||||
const instantiationService = new InstantiationService();
|
||||
private _createServices(): [IConfigurationService, IWorkspaceService] {
|
||||
const container = new BeanContainer();
|
||||
|
||||
const configurationService = new ConfigurationService();
|
||||
instantiationService.container.set(IConfigurationService, configurationService);
|
||||
container.set(IConfigurationService, configurationService);
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
container.set(IWorkspaceService, workspaceService);
|
||||
|
||||
container.set(IFileService, new FileService());
|
||||
|
||||
container.set(IWindowService, new CtorDescriptor(WindowService));
|
||||
|
||||
this.instantiationService = new InstantiationService(container);
|
||||
|
||||
return [configurationService, workspaceService];
|
||||
}
|
||||
|
||||
initServices() {}
|
||||
private async _initServices() {
|
||||
const [configurationService, workspaceService] = this._createServices();
|
||||
|
||||
await configurationService.initialize();
|
||||
// init workspace
|
||||
await workspaceService.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
export async function createLowCodeEngineApp() {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './window';
|
||||
export * from './windowService';
|
|
@ -0,0 +1,79 @@
|
|||
import { type IDisposable, type Events } from '@alilc/lowcode-shared';
|
||||
import { URI } from '../common';
|
||||
import { IWorkspaceIdentifier } from '../workspace';
|
||||
|
||||
export const enum WindowMode {
|
||||
Maximized,
|
||||
Normal,
|
||||
Fullscreen,
|
||||
Custom,
|
||||
}
|
||||
|
||||
export interface IWindowState {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
mode?: WindowMode;
|
||||
zoomLevel?: number;
|
||||
readonly display?: number;
|
||||
}
|
||||
|
||||
export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState {
|
||||
return {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
mode,
|
||||
};
|
||||
};
|
||||
|
||||
export interface IEditOptions {}
|
||||
|
||||
export interface IPath<T = IEditOptions> {
|
||||
/**
|
||||
* The file path to open within the instance
|
||||
*/
|
||||
readonly fileUri: URI;
|
||||
|
||||
/**
|
||||
* A hint that the file exists. if true, the
|
||||
* file exists, if false it does not. with
|
||||
* `undefined` the state is unknown.
|
||||
*/
|
||||
readonly exists?: boolean;
|
||||
|
||||
/**
|
||||
* Optional editor options to apply in the file
|
||||
*/
|
||||
options?: T;
|
||||
}
|
||||
|
||||
export interface IWindowConfiguration {
|
||||
fileToOpenOrCreate: IPath;
|
||||
|
||||
workspace?: IWorkspaceIdentifier;
|
||||
}
|
||||
|
||||
export interface IEditWindow extends IDisposable {
|
||||
readonly onWillLoad: Events.Event<void>;
|
||||
readonly onDidSignalReady: Events.Event<void>;
|
||||
readonly onDidDestroy: Events.Event<void>;
|
||||
readonly onDidClose: Events.Event<void>;
|
||||
|
||||
readonly id: number;
|
||||
|
||||
readonly config: IWindowConfiguration | undefined;
|
||||
|
||||
readonly lastFocusTime: number;
|
||||
focus(): void;
|
||||
|
||||
readonly isReady: boolean;
|
||||
ready(): Promise<IEditWindow>;
|
||||
|
||||
load(config: IWindowConfiguration, options?: { isReload?: boolean }): void;
|
||||
reload(): void;
|
||||
|
||||
close(): void;
|
||||
|
||||
sendWhenReady(channel: string, ...args: any[]): void;
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import { Disposable, Events } from '@alilc/lowcode-shared';
|
||||
import { IWindowState, IEditWindow, IWindowConfiguration } from './window';
|
||||
import { IFileService } from '../file';
|
||||
|
||||
export interface IWindowCreationOptions {
|
||||
readonly state: IWindowState;
|
||||
}
|
||||
|
||||
export class EditWindow extends Disposable implements IEditWindow {
|
||||
private readonly _onWillLoad = this._addDispose(new Events.Emitter<void>());
|
||||
onWillLoad = this._onWillLoad.event;
|
||||
|
||||
private readonly _onDidSignalReady = this._addDispose(new Events.Emitter<void>());
|
||||
onDidSignalReady = this._onDidSignalReady.event;
|
||||
|
||||
private readonly _onDidClose = this._addDispose(new Events.Emitter<void>());
|
||||
onDidClose = this._onDidClose.event;
|
||||
|
||||
private readonly _onDidDestroy = this._addDispose(new Events.Emitter<void>());
|
||||
onDidDestroy = this._onDidDestroy.event;
|
||||
|
||||
private _id: number;
|
||||
get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
private _windowState: IWindowState;
|
||||
|
||||
private readonly _whenReadyCallbacks: ((window: IEditWindow) => void)[] = [];
|
||||
|
||||
private _readyState: boolean;
|
||||
get isReady(): boolean {
|
||||
return this._readyState;
|
||||
}
|
||||
|
||||
private _lastFocusTime;
|
||||
get lastFocusTime(): number {
|
||||
return this._lastFocusTime;
|
||||
}
|
||||
|
||||
private _config: IWindowConfiguration | undefined;
|
||||
get config(): IWindowConfiguration | undefined {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
constructor(
|
||||
options: IWindowCreationOptions,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._windowState = options.state;
|
||||
|
||||
this._id = 0;
|
||||
|
||||
this._lastFocusTime = Date.now();
|
||||
}
|
||||
|
||||
ready(): Promise<IEditWindow> {
|
||||
return new Promise<IEditWindow>((resolve) => {
|
||||
if (this.isReady) {
|
||||
return resolve(this);
|
||||
}
|
||||
|
||||
// otherwise keep and call later when we are ready
|
||||
this._whenReadyCallbacks.push(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private setReady(): void {
|
||||
this._readyState = true;
|
||||
|
||||
// inform all waiting promises that we are ready now
|
||||
while (this._whenReadyCallbacks.length) {
|
||||
this._whenReadyCallbacks.pop()!(this);
|
||||
}
|
||||
}
|
||||
|
||||
load(config: IWindowConfiguration): void {
|
||||
this._onWillLoad.notify();
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
reload(): void {}
|
||||
|
||||
focus(): void {}
|
||||
|
||||
close(): void {}
|
||||
|
||||
sendWhenReady(channel: string, ...args: any[]): void {
|
||||
if (this.isReady) {
|
||||
this.send(channel, ...args);
|
||||
} else {
|
||||
this.ready().then(() => {
|
||||
this.send(channel, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private send(channel: string, ...args: any[]): void {
|
||||
// todo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import { createDecorator, Disposable, Events, IInstantiationService } from '@alilc/lowcode-shared';
|
||||
import { defaultWindowState, IEditWindow, IWindowConfiguration } from './window';
|
||||
import { Schemas, URI } from '../common';
|
||||
import { EditWindow } from './windowImpl';
|
||||
import { IFileService } from '../file';
|
||||
|
||||
export interface IOpenConfiguration {
|
||||
readonly urisToOpen: IWindowOpenable[];
|
||||
|
||||
readonly forceNewWindow?: boolean;
|
||||
|
||||
/**
|
||||
* Specifies if the file should be only be opened
|
||||
* if it exists.
|
||||
*/
|
||||
readonly openOnlyIfExists?: boolean;
|
||||
|
||||
addMode?: boolean;
|
||||
}
|
||||
|
||||
export interface IWindowOpenable {
|
||||
label?: string;
|
||||
readonly fileUri: URI;
|
||||
}
|
||||
|
||||
export interface IWindowService {
|
||||
readonly onDidOpenWindow: Events.Event<IEditWindow>;
|
||||
// readonly onDidChangeFullScreen: Events.Event<{ window: IEditWindow; fullscreen: boolean }>;
|
||||
// readonly onDidDestroyWindow: Events.Event<IEditWindow>;
|
||||
|
||||
open(openConfig: IOpenConfiguration): Promise<IEditWindow[]>;
|
||||
|
||||
// sendToFocused(channel: string, ...args: any[]): void;
|
||||
// sendToOpeningWindow(channel: string, ...args: any[]): void;
|
||||
// sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void;
|
||||
|
||||
getWindows(): IEditWindow[];
|
||||
getWindowCount(): number;
|
||||
|
||||
getLastActiveWindow(): IEditWindow | undefined;
|
||||
|
||||
getWindowById(windowId: number): IEditWindow | undefined;
|
||||
}
|
||||
|
||||
export const IWindowService = createDecorator<IWindowService>('windowService');
|
||||
|
||||
export class WindowService extends Disposable implements IWindowService {
|
||||
private _onDidOpenWindow = this._addDispose(new Events.Emitter<IEditWindow>());
|
||||
onDidOpenWindow = this._onDidOpenWindow.event;
|
||||
|
||||
private readonly windows = new Map<number, IEditWindow>();
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getWindows(): IEditWindow[] {
|
||||
return [...this.windows.values()];
|
||||
}
|
||||
|
||||
getWindowCount(): number {
|
||||
return this.windows.size;
|
||||
}
|
||||
|
||||
getWindowById(windowId: number): IEditWindow | undefined {
|
||||
return this.windows.get(windowId);
|
||||
}
|
||||
|
||||
getLastActiveWindow(): IEditWindow | undefined {
|
||||
let lastFocusedWindow: IEditWindow | undefined = undefined;
|
||||
let maxLastFocusTime = Number.MIN_VALUE;
|
||||
|
||||
const windows = this.getWindows();
|
||||
for (const window of windows) {
|
||||
if (window.lastFocusTime > maxLastFocusTime) {
|
||||
maxLastFocusTime = window.lastFocusTime;
|
||||
lastFocusedWindow = window;
|
||||
}
|
||||
}
|
||||
|
||||
return lastFocusedWindow;
|
||||
}
|
||||
|
||||
async open(openConfig: IOpenConfiguration): Promise<IEditWindow[]> {
|
||||
return this._doOpen(openConfig);
|
||||
}
|
||||
|
||||
private async _doOpen(openConfig: IOpenConfiguration): Promise<IEditWindow[]> {
|
||||
const usedWindows: IEditWindow[] = [];
|
||||
const { urisToOpen, openOnlyIfExists } = openConfig;
|
||||
|
||||
for (const item of urisToOpen) {
|
||||
const fs = this.fileService.getProvider(Schemas.file)!;
|
||||
|
||||
let exists = false;
|
||||
try {
|
||||
await fs.access(item.fileUri);
|
||||
exists = true;
|
||||
} catch {
|
||||
if (openOnlyIfExists) continue;
|
||||
}
|
||||
|
||||
const config: IWindowConfiguration = {
|
||||
fileToOpenOrCreate: {
|
||||
fileUri: item.fileUri,
|
||||
exists,
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
const window = await this._openInEditWindow(config);
|
||||
|
||||
usedWindows.push(window);
|
||||
}
|
||||
|
||||
return usedWindows;
|
||||
}
|
||||
|
||||
private async _openInEditWindow(config: IWindowConfiguration): Promise<IEditWindow> {
|
||||
const newWindow = this.instantiationService.createInstance(EditWindow, defaultWindowState());
|
||||
|
||||
this.windows.set(newWindow.id, newWindow);
|
||||
|
||||
// Indicate new window via event
|
||||
this._onDidOpenWindow.notify(newWindow);
|
||||
|
||||
newWindow.load(config);
|
||||
|
||||
return newWindow;
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from './workbenchService';
|
||||
export * from './workbench';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared';
|
||||
import { Disposable, Events } from '@alilc/lowcode-shared';
|
||||
import { IWidget } from './widget';
|
||||
import { Extensions, Registry } from '../../extension/registry';
|
||||
|
||||
export interface IWidgetRegistry<View> {
|
||||
onDidRegister: Event<IWidget<View>[]>;
|
||||
onDidRegister: Events.Event<IWidget<View>[]>;
|
||||
|
||||
registerWidget(widget: IWidget<View>): string;
|
||||
|
||||
|
@ -12,13 +12,15 @@ export interface IWidgetRegistry<View> {
|
|||
getWidgets(): IWidget<View>[];
|
||||
}
|
||||
|
||||
export class WidgetRegistryImpl<View> implements IWidgetRegistry<View> {
|
||||
export class WidgetRegistryImpl<View> extends Disposable implements IWidgetRegistry<View> {
|
||||
private _widgets: Map<string, IWidget<View>> = new Map();
|
||||
|
||||
private emitter = new Emitter<IWidget<View>[]>();
|
||||
private _onDidRegister = this._addDispose(new Events.Emitter<IWidget<View>[]>());
|
||||
|
||||
onDidRegister(fn: EventListener<IWidget<View>[]>) {
|
||||
return this.emitter.on(fn);
|
||||
onDidRegister = this._onDidRegister.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getWidgets(): IWidget<View>[] {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export const enum WorkbenchState {
|
||||
EMPTY = 1,
|
||||
FOLDER,
|
||||
WORKSPACE /* preset */,
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
import { URI } from '../../common/uri';
|
||||
|
||||
export enum FileType {
|
||||
/**
|
||||
* File is unknown (neither file, directory).
|
||||
*/
|
||||
Unknown = 0,
|
||||
|
||||
/**
|
||||
* File is a normal file.
|
||||
*/
|
||||
File = 1,
|
||||
|
||||
/**
|
||||
* File is a directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
}
|
||||
|
||||
export interface IStat {
|
||||
/**
|
||||
* The file type.
|
||||
*/
|
||||
readonly type: FileType;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
*/
|
||||
readonly mtime: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
*/
|
||||
readonly ctime: number;
|
||||
}
|
||||
|
||||
export interface IBaseFileStat {
|
||||
/**
|
||||
* The unified resource identifier of this file or folder.
|
||||
*/
|
||||
readonly resource: URI;
|
||||
|
||||
/**
|
||||
* The name which is the last segment
|
||||
* of the {{path}}.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The size of the file.
|
||||
*
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* The last modification date represented as millis from unix epoch.
|
||||
*
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
readonly mtime?: number;
|
||||
|
||||
/**
|
||||
* The creation date represented as millis from unix epoch.
|
||||
*
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
readonly ctime?: number;
|
||||
|
||||
/**
|
||||
* A unique identifier that represents the
|
||||
* current state of the file or directory.
|
||||
*
|
||||
* The value may or may not be resolved as
|
||||
* it is optional.
|
||||
*/
|
||||
readonly etag?: string;
|
||||
|
||||
/**
|
||||
* File is readonly. Components like editors should not
|
||||
* offer to edit the contents.
|
||||
*/
|
||||
readonly readonly?: boolean;
|
||||
|
||||
/**
|
||||
* File is locked. Components like editors should offer
|
||||
* to edit the contents and ask the user upon saving to
|
||||
* remove the lock.
|
||||
*/
|
||||
readonly locked?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file resource with meta information and resolved children if any.
|
||||
*/
|
||||
export interface IFileStat extends IBaseFileStat {
|
||||
/**
|
||||
* The resource is a file.
|
||||
*/
|
||||
readonly isFile: boolean;
|
||||
|
||||
/**
|
||||
* The resource is a directory.
|
||||
*/
|
||||
readonly isDirectory: boolean;
|
||||
|
||||
/**
|
||||
* The children of the file stat or undefined if none.
|
||||
*/
|
||||
children: IFileStat[] | undefined;
|
||||
}
|
||||
|
||||
export interface IFileStatWithMetadata extends Required<IFileStat> {
|
||||
readonly children: IFileStatWithMetadata[];
|
||||
}
|
||||
|
||||
export const enum FileOperation {
|
||||
CREATE,
|
||||
DELETE,
|
||||
MOVE,
|
||||
COPY,
|
||||
WRITE,
|
||||
}
|
||||
|
||||
export interface IFileOperationEvent {
|
||||
readonly resource: URI;
|
||||
readonly operation: FileOperation;
|
||||
|
||||
isOperation(operation: FileOperation.DELETE | FileOperation.WRITE): boolean;
|
||||
isOperation(
|
||||
operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY,
|
||||
): this is IFileOperationEventWithMetadata;
|
||||
}
|
||||
|
||||
export interface IFileOperationEventWithMetadata extends IFileOperationEvent {
|
||||
readonly target: IFileStatWithMetadata;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
/**
|
||||
* URI -> file content
|
||||
* URI -> file Stat
|
||||
*/
|
||||
|
||||
export interface IFileManagement {}
|
|
@ -1 +0,0 @@
|
|||
export interface IFileService {}
|
|
@ -1,26 +0,0 @@
|
|||
import { URI } from '../common/uri';
|
||||
|
||||
export interface IWorkspaceFolderData {
|
||||
/**
|
||||
* The associated URI for this workspace folder.
|
||||
*/
|
||||
readonly uri: URI;
|
||||
|
||||
/**
|
||||
* The name of this workspace folder. Defaults to
|
||||
* the basename of its [uri-path](#Uri.path)
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The ordinal number of this workspace folder.
|
||||
*/
|
||||
readonly index: number;
|
||||
}
|
||||
|
||||
export interface IWorkspaceFolder extends IWorkspaceFolderData {
|
||||
/**
|
||||
* Given workspace folder relative path, returns the resource with the absolute path.
|
||||
*/
|
||||
toResource: (relativePath: string) => URI;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './workspaceService';
|
||||
export * from './workspace';
|
||||
export * from './workspaceFolder';
|
|
@ -1,82 +0,0 @@
|
|||
import { type Event } from '@alilc/lowcode-shared';
|
||||
import { URI } from '../../common/uri';
|
||||
|
||||
export interface IEditWindow {
|
||||
// readonly onWillLoad: Event<ILoadEvent>;
|
||||
readonly onDidSignalReady: Event<void>;
|
||||
readonly onDidDestroy: Event<void>;
|
||||
|
||||
readonly onDidClose: Event<void>;
|
||||
|
||||
readonly id: number;
|
||||
|
||||
readonly config: IWindowConfiguration | undefined;
|
||||
|
||||
readonly isReady: boolean;
|
||||
ready(): Promise<IEditWindow>;
|
||||
|
||||
load(config: IWindowConfiguration, options?: { isReload?: boolean }): void;
|
||||
reload(): void;
|
||||
}
|
||||
|
||||
export interface IWindowConfiguration {
|
||||
filesToOpenOrCreate?: IPath[];
|
||||
}
|
||||
|
||||
export interface IPath<T = any> {
|
||||
/**
|
||||
* Optional editor options to apply in the file
|
||||
*/
|
||||
readonly options?: T;
|
||||
|
||||
/**
|
||||
* The file path to open within the instance
|
||||
*/
|
||||
fileUri?: URI;
|
||||
|
||||
/**
|
||||
* Specifies if the file should be only be opened
|
||||
* if it exists.
|
||||
*/
|
||||
readonly openOnlyIfExists?: boolean;
|
||||
}
|
||||
|
||||
export const enum WindowMode {
|
||||
Maximized,
|
||||
Normal,
|
||||
Fullscreen,
|
||||
Custom,
|
||||
}
|
||||
|
||||
export interface IWindowState {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
mode?: WindowMode;
|
||||
zoomLevel?: number;
|
||||
readonly display?: number;
|
||||
}
|
||||
|
||||
export interface IOpenConfiguration {
|
||||
readonly urisToOpen?: IWindowOpenable[];
|
||||
readonly preferNewWindow?: boolean;
|
||||
readonly forceNewWindow?: boolean;
|
||||
readonly forceNewTabbedWindow?: boolean;
|
||||
readonly forceReuseWindow?: boolean;
|
||||
readonly forceEmpty?: boolean;
|
||||
}
|
||||
|
||||
export interface IBaseWindowOpenable {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IFolderToOpen extends IBaseWindowOpenable {
|
||||
readonly folderUri: URI;
|
||||
}
|
||||
|
||||
export interface IFileToOpen extends IBaseWindowOpenable {
|
||||
readonly fileUri: URI;
|
||||
}
|
||||
|
||||
export type IWindowOpenable = IFolderToOpen | IFileToOpen;
|
|
@ -1,43 +0,0 @@
|
|||
import { type Event } from '@alilc/lowcode-shared';
|
||||
import { IEditWindow, IOpenConfiguration } from './window';
|
||||
|
||||
export interface IWindowService {
|
||||
readonly onDidOpenWindow: Event<IEditWindow>;
|
||||
readonly onDidSignalReadyWindow: Event<IEditWindow>;
|
||||
readonly onDidChangeFullScreen: Event<{ window: IEditWindow; fullscreen: boolean }>;
|
||||
readonly onDidDestroyWindow: Event<IEditWindow>;
|
||||
|
||||
open(openConfig: IOpenConfiguration): Promise<IEditWindow[]>;
|
||||
|
||||
sendToFocused(channel: string, ...args: any[]): void;
|
||||
sendToOpeningWindow(channel: string, ...args: any[]): void;
|
||||
sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void;
|
||||
|
||||
getWindows(): IEditWindow[];
|
||||
getWindowCount(): number;
|
||||
|
||||
getFocusedWindow(): IEditWindow | undefined;
|
||||
getLastActiveWindow(): IEditWindow | undefined;
|
||||
|
||||
getWindowById(windowId: number): IEditWindow | undefined;
|
||||
}
|
||||
|
||||
export class WindowService implements IWindowService {
|
||||
private readonly windows = new Map<number, IEditWindow>();
|
||||
|
||||
getWindows(): IEditWindow[] {
|
||||
return [...this.windows.values()];
|
||||
}
|
||||
|
||||
getWindowCount(): number {
|
||||
return this.windows.size;
|
||||
}
|
||||
|
||||
getFocusedWindow(): IEditWindow | undefined {
|
||||
return this.getWindows().find((w) => w.focused);
|
||||
}
|
||||
|
||||
getLastActiveWindow(): IEditWindow | undefined {
|
||||
return this.getWindows().find((w) => w.lastActive);
|
||||
}
|
||||
}
|
|
@ -1,31 +1,92 @@
|
|||
import { IWorkspaceFolder } from './folder';
|
||||
import { URI, basename, TernarySearchTree } from '../common';
|
||||
import { IWorkspaceFolder, WorkspaceFolder } from './workspaceFolder';
|
||||
|
||||
export interface IWorkspaceIdentifier {
|
||||
/**
|
||||
* Every workspace (multi-root, single folder or empty)
|
||||
* has a unique identifier. It is not possible to open
|
||||
* a workspace with the same `id` in multiple windows
|
||||
*/
|
||||
readonly id: string;
|
||||
/**
|
||||
* Folder path as `URI`.
|
||||
*/
|
||||
readonly uri: URI;
|
||||
}
|
||||
|
||||
export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier {
|
||||
const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined;
|
||||
|
||||
return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.uri);
|
||||
}
|
||||
|
||||
export function toWorkspaceIdentifier(pathOrWorkspace: IWorkspace | string): IWorkspaceIdentifier {
|
||||
if (typeof pathOrWorkspace === 'string') {
|
||||
return {
|
||||
id: basename(pathOrWorkspace),
|
||||
uri: URI.from({ path: pathOrWorkspace }),
|
||||
};
|
||||
}
|
||||
|
||||
const workspace = pathOrWorkspace;
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
uri: workspace.uri,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* workspace -> one or more folders -> virtual files
|
||||
* file -> editWindow
|
||||
* editorView -> component tree schema
|
||||
*
|
||||
* project = (one or muti folders -> files) + some configs
|
||||
*/
|
||||
export interface IWorkspace {
|
||||
readonly id: string;
|
||||
|
||||
readonly uri: URI;
|
||||
/**
|
||||
* Folders in the workspace.
|
||||
*/
|
||||
readonly folders: IWorkspaceFolder[];
|
||||
folders: IWorkspaceFolder[];
|
||||
}
|
||||
|
||||
export class Workspace implements IWorkspace {
|
||||
private _folders: IWorkspaceFolder[] = [];
|
||||
private _folders: WorkspaceFolder[];
|
||||
private _foldersMap: TernarySearchTree<string, WorkspaceFolder>;
|
||||
|
||||
constructor(private _id: string) {}
|
||||
constructor(
|
||||
private _id: string,
|
||||
private _uri: URI,
|
||||
folders: WorkspaceFolder[],
|
||||
private _ignorePathCasing = false,
|
||||
) {
|
||||
this.folders = folders;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
get folders() {
|
||||
return this._folders;
|
||||
}
|
||||
|
||||
set folders(folders: WorkspaceFolder[]) {
|
||||
this._folders = folders;
|
||||
this._updateFoldersMap();
|
||||
}
|
||||
|
||||
getFolder(resource: URI): IWorkspaceFolder | null {
|
||||
if (!resource) {
|
||||
return null;
|
||||
}
|
||||
return this._foldersMap.findSubstr(resource.path) || null;
|
||||
}
|
||||
|
||||
private _updateFoldersMap(): void {
|
||||
this._foldersMap = TernarySearchTree.forPaths<WorkspaceFolder>(this._ignorePathCasing);
|
||||
for (const folder of this.folders) {
|
||||
this._foldersMap.set(folder.uri.path, folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { URI, basename } from '../common';
|
||||
|
||||
export interface IWorkspaceFolderData {
|
||||
/**
|
||||
* The associated URI for this workspace folder.
|
||||
*/
|
||||
readonly uri: URI;
|
||||
|
||||
/**
|
||||
* The name of this workspace folder. Defaults to
|
||||
* the basename of its [uri-path](#Uri.path)
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The ordinal number of this workspace folder.
|
||||
*/
|
||||
readonly index: number;
|
||||
}
|
||||
|
||||
export interface IWorkspaceFolder extends IWorkspaceFolderData {
|
||||
/**
|
||||
* Given workspace folder relative path, returns the resource with the absolute path.
|
||||
*/
|
||||
toResource: (relativePath: string) => URI;
|
||||
}
|
||||
|
||||
export class WorkspaceFolder implements IWorkspaceFolder {
|
||||
readonly uri: URI;
|
||||
readonly name: string;
|
||||
readonly index: number;
|
||||
|
||||
constructor(data: IWorkspaceFolderData) {
|
||||
this.uri = data.uri;
|
||||
this.name = data.name;
|
||||
this.index = data.index;
|
||||
}
|
||||
|
||||
toResource(relativePath: string) {
|
||||
return URI.joinPath(this.uri, relativePath);
|
||||
}
|
||||
|
||||
toJSON(): IWorkspaceFolderData {
|
||||
return { uri: this.uri, name: this.name, index: this.index };
|
||||
}
|
||||
}
|
||||
|
||||
export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder {
|
||||
const candidate = thing as IWorkspaceFolder;
|
||||
|
||||
return !!(
|
||||
candidate &&
|
||||
typeof candidate === 'object' &&
|
||||
URI.isUri(candidate.uri) &&
|
||||
typeof candidate.name === 'string' &&
|
||||
typeof candidate.toResource === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export function toWorkspaceFolder(resource: URI): WorkspaceFolder {
|
||||
return new WorkspaceFolder({ uri: resource, index: 0, name: basename(resource.path) });
|
||||
}
|
|
@ -1,7 +1,42 @@
|
|||
import { createDecorator } from '@alilc/lowcode-shared';
|
||||
import { createDecorator, Disposable } from '@alilc/lowcode-shared';
|
||||
import { Workspace, type IWorkspaceIdentifier, isWorkspaceIdentifier } from './workspace';
|
||||
import { toWorkspaceFolder, IWorkspaceFolder } from './workspaceFolder';
|
||||
import { URI } from '../common';
|
||||
|
||||
export interface IWorkspaceService {}
|
||||
export interface IWorkspaceService {
|
||||
initialize(): Promise<void>;
|
||||
|
||||
enterWorkspace(identifier: IWorkspaceIdentifier): Promise<void>;
|
||||
|
||||
getWorkspace(): Workspace;
|
||||
|
||||
getWorkspaceFolder(resource: URI): IWorkspaceFolder | null;
|
||||
}
|
||||
|
||||
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
||||
|
||||
export class WorkspaceService implements IWorkspaceService {}
|
||||
export class WorkspaceService extends Disposable implements IWorkspaceService {
|
||||
private _workspace: Workspace;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async initialize() {}
|
||||
|
||||
async enterWorkspace(identifier: IWorkspaceIdentifier) {
|
||||
if (!isWorkspaceIdentifier(identifier)) {
|
||||
throw new Error('Invalid workspace identifier');
|
||||
}
|
||||
|
||||
this._workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]);
|
||||
}
|
||||
|
||||
getWorkspace(): Workspace {
|
||||
return this._workspace;
|
||||
}
|
||||
|
||||
getWorkspaceFolder(resource: URI) {
|
||||
return this._workspace.getFolder(resource);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "src/common/uri.ts"]
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { type StringDictionary, type ComponentTree } from '@alilc/lowcode-shared';
|
||||
import { CodeRuntime } from '@alilc/lowcode-renderer-core';
|
||||
import { FunctionComponent, ComponentType } from 'react';
|
||||
import { reactiveStateFactory } from '../app/reactiveState';
|
||||
import {
|
||||
type LowCodeComponentProps,
|
||||
createComponent as createSchemaComponent,
|
||||
type ComponentOptions as SchemaComponentOptions,
|
||||
reactiveStateFactory,
|
||||
} from '../runtime';
|
||||
} from '../runtime/createComponent';
|
||||
import { type ComponentsAccessor } from '../app';
|
||||
|
||||
export interface ComponentOptions extends SchemaComponentOptions {
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
type ReactRendererBoostsApi,
|
||||
} from './boosts';
|
||||
import { createAppView } from './components/view';
|
||||
import { ComponentOptions } from '../runtime';
|
||||
import { type ComponentOptions } from '../runtime/createComponent';
|
||||
|
||||
import type { Project, Package } from '@alilc/lowcode-shared';
|
||||
import type {
|
||||
|
@ -185,8 +185,6 @@ export class App extends Disposable implements IRendererApplication {
|
|||
await extensionHostService.registerPlugin(this.options.plugins ?? []);
|
||||
|
||||
await packageManagementService.loadPackages(this.options.packages ?? []);
|
||||
|
||||
lifeCycleService.setPhase(LifecyclePhase.Ready);
|
||||
}
|
||||
|
||||
private async _createRouter() {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useAppContext } from '../context';
|
|||
import { OutletProps } from '../boosts';
|
||||
import { useRouteLocation } from '../context';
|
||||
import { createComponent } from '../../runtime/createComponent';
|
||||
import { reactiveStateFactory } from '../../runtime/reactiveState';
|
||||
import { reactiveStateFactory } from '../reactiveState';
|
||||
|
||||
export function RouteOutlet(props: OutletProps) {
|
||||
const app = useAppContext();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { AppContext } from '../context';
|
||||
import { type App } from '../app';
|
||||
import { getOrCreateComponent, reactiveStateFactory } from '../../runtime';
|
||||
import { reactiveStateFactory } from '../reactiveState';
|
||||
import { getOrCreateComponent } from '../../runtime/createComponent';
|
||||
import { RouterView } from './routerView';
|
||||
import { RouteOutlet } from './route';
|
||||
import { type WrapperComponent, type Outlet } from '../boosts';
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
type ReactNode,
|
||||
} from 'react';
|
||||
import { ComponentsAccessor } from '../app';
|
||||
import { useReactiveStore } from './hooks/useReactiveStore';
|
||||
import { useReactiveStore } from './useReactiveStore';
|
||||
import { getOrCreateComponent, type ComponentOptions } from './createComponent';
|
||||
|
||||
export type ReactComponent = ComponentType<any>;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export * from './createComponent';
|
||||
export * from './reactiveState';
|
||||
export * from './elements';
|
|
@ -15,11 +15,9 @@
|
|||
"scripts": {
|
||||
"build:target": "vite build",
|
||||
"build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.mjs",
|
||||
"test": "vitest",
|
||||
"test:cov": "build-scripts test --config build.test.json --jest-coverage"
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alilc/lowcode-designer": "workspace:*",
|
||||
"@alilc/lowcode-react-renderer": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
export default {
|
||||
id: 'node_ockvuu8u911',
|
||||
css: 'body{background-color:#f2f3f5}',
|
||||
flows: [],
|
||||
props: {
|
||||
className: 'page_kvuu9hym',
|
||||
pageStyle: {
|
||||
backgroundColor: '#f2f3f5',
|
||||
},
|
||||
containerStyle: {},
|
||||
templateVersion: '1.0.0',
|
||||
},
|
||||
state: {},
|
||||
title: '',
|
||||
methods: {
|
||||
__initMethods__: {
|
||||
type: 'JSExpression',
|
||||
value: "function (exports, module) { \"use strict\";\n\nexports.__esModule = true;\nexports.func1 = func1;\nexports.helloPage = helloPage;\n\nfunction func1() {\n console.info('hello, this is a page function');\n}\n\nfunction helloPage() {\n // 你可以这么调用其他函数\n this.func1(); // 你可以这么调用组件的函数\n // this.$('textField_xxx').getValue();\n // 你可以这么使用「数据源面板」定义的「变量」\n // this.state.xxx\n // 你可以这么发送一个在「数据源面板」定义的「远程 API」\n // this.dataSourceMap['xxx'].load(data)\n // API 详见:https://go.alibaba-inc.com/help3/API\n} \n}",
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'node_ockvuu8u915',
|
||||
props: {
|
||||
fieldId: 'div_kvuu9gl1',
|
||||
behavior: 'NORMAL',
|
||||
__style__: {},
|
||||
customClassName: '',
|
||||
useFieldIdAsDomId: false,
|
||||
},
|
||||
title: '',
|
||||
children: [
|
||||
{
|
||||
id: 'node_ockvuu8u916',
|
||||
props: {
|
||||
content: {
|
||||
use: 'zh-CN',
|
||||
type: 'JSExpression',
|
||||
'en-US': 'Tips content',
|
||||
value: '"我是一个简单的测试页面"',
|
||||
'zh-CN': '我是一个简单的测试页面',
|
||||
extType: 'i18n',
|
||||
},
|
||||
fieldId: 'text_kvuu9gl2',
|
||||
maxLine: 0,
|
||||
behavior: 'NORMAL',
|
||||
__style__: {},
|
||||
showTitle: false,
|
||||
},
|
||||
title: '',
|
||||
condition: true,
|
||||
componentName: 'Text',
|
||||
},
|
||||
],
|
||||
condition: true,
|
||||
componentName: 'Div',
|
||||
},
|
||||
],
|
||||
condition: true,
|
||||
dataSource: {
|
||||
list: [],
|
||||
sync: true,
|
||||
online: [],
|
||||
offline: [],
|
||||
globalConfig: {
|
||||
fit: {
|
||||
type: 'JSExpression',
|
||||
value: "function main(){\n 'use strict';\n\nvar __compiledFunc__ = function fit(response) {\n var content = response.content !== undefined ? response.content : response;\n var error = {\n message: response.errorMsg || response.errors && response.errors[0] && response.errors[0].msg || response.content || '远程数据源请求出错,success is false'\n };\n var success = true;\n if (response.success !== undefined) {\n success = response.success;\n } else if (response.hasError !== undefined) {\n success = !response.hasError;\n }\n return {\n content: content,\n success: success,\n error: error\n };\n};\n return __compiledFunc__.apply(this, arguments);\n}",
|
||||
extType: 'function',
|
||||
},
|
||||
},
|
||||
},
|
||||
lifeCycles: {
|
||||
constructor: {
|
||||
type: 'JSExpression',
|
||||
value: "function constructor() {\nvar module = { exports: {} };\nvar _this = this;\nthis.__initMethods__(module.exports, module);\nObject.keys(module.exports).forEach(function(item) {\n if(typeof module.exports[item] === 'function'){\n _this[item] = module.exports[item];\n }\n});\n\n}",
|
||||
extType: 'function',
|
||||
},
|
||||
},
|
||||
componentName: 'Page',
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Base should be render NotFoundComponent 1`] = `
|
||||
<div
|
||||
className="lce-page page_kvuu9hym"
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
componentName="Div"
|
||||
>
|
||||
<div
|
||||
componentName="Text"
|
||||
>
|
||||
Text Component Not Found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Base should be render Text 1`] = `
|
||||
<div
|
||||
className="lce-page page_kvuu9hym"
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
componentName="Div"
|
||||
>
|
||||
<div
|
||||
__designMode="design"
|
||||
__style__={Object {}}
|
||||
behavior="NORMAL"
|
||||
componentId="node_ockvuu8u916"
|
||||
fieldId="text_kvuu9gl2"
|
||||
forwardRef={[Function]}
|
||||
maxLine={0}
|
||||
showTitle={false}
|
||||
>
|
||||
我是一个简单的测试页面
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,31 +0,0 @@
|
|||
import renderer from 'react-test-renderer';
|
||||
import rendererContainer from '../../../src/renderer';
|
||||
import SimulatorRendererView from '../../../src/renderer-view';
|
||||
import { Text } from '../../utils/components';
|
||||
|
||||
describe('Base', () => {
|
||||
const component = renderer.create(
|
||||
<SimulatorRendererView
|
||||
rendererContainer={rendererContainer}
|
||||
/>
|
||||
);
|
||||
|
||||
it('should be render NotFoundComponent', () => {
|
||||
let tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be render Text', () => {
|
||||
// 更新 _componentsMap 值
|
||||
(rendererContainer as any)._componentsMap.Text = Text;// = host.designer.componentsMap;
|
||||
// 更新 components 列表
|
||||
(rendererContainer as any).buildComponents();
|
||||
|
||||
expect(!!(rendererContainer.components as any).Text).toBeTruthy();
|
||||
|
||||
rendererContainer.rerender();
|
||||
|
||||
let tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
})
|
|
@ -1,7 +0,0 @@
|
|||
export const Text = ({
|
||||
__tag,
|
||||
content,
|
||||
...props
|
||||
}: any) => (<div {...props}>{content}</div>);
|
||||
|
||||
export const Page = (props: any) => (<div>{props.children}</div>);
|
|
@ -1,79 +0,0 @@
|
|||
import { Box, Breadcrumb, Form, Select, Input, Button, Table, Pagination, Dialog } from '@alifd/next';
|
||||
import defaultSchema from '../schema/basic';
|
||||
import { Page } from './components';
|
||||
|
||||
class Designer {
|
||||
componentsMap = {
|
||||
Box,
|
||||
Breadcrumb,
|
||||
'Breadcrumb.Item': Breadcrumb.Item,
|
||||
Form,
|
||||
'Form.Item': Form.Item,
|
||||
Select,
|
||||
Input,
|
||||
Button,
|
||||
'Button.Group': Button.Group,
|
||||
Table,
|
||||
Pagination,
|
||||
Dialog,
|
||||
Page,
|
||||
}
|
||||
}
|
||||
|
||||
class Host {
|
||||
designer = new Designer();
|
||||
|
||||
connect = () => {}
|
||||
|
||||
autorun = (fn: Function) => {
|
||||
fn();
|
||||
}
|
||||
|
||||
autoRender = true;
|
||||
|
||||
componentsConsumer = {
|
||||
consume() {}
|
||||
}
|
||||
|
||||
schema = defaultSchema;
|
||||
|
||||
project = {
|
||||
documents: [
|
||||
{
|
||||
id: '1',
|
||||
path: '/',
|
||||
fileName: '',
|
||||
export: () => {
|
||||
return this.schema;
|
||||
},
|
||||
getNode: () => {},
|
||||
}
|
||||
],
|
||||
get: () => ({}),
|
||||
}
|
||||
|
||||
setInstance() {}
|
||||
|
||||
designMode = 'design'
|
||||
|
||||
get() {}
|
||||
|
||||
injectionConsumer = {
|
||||
consume() {}
|
||||
}
|
||||
|
||||
i18nConsumer = {
|
||||
consume() {}
|
||||
}
|
||||
|
||||
/** 下列的函数或者方法是方便测试用 */
|
||||
mockSchema = (schema: any) => {
|
||||
this.schema = schema;
|
||||
};
|
||||
}
|
||||
|
||||
if (!(window as any).LCSimulatorHost) {
|
||||
(window as any).LCSimulatorHost = new Host();
|
||||
}
|
||||
|
||||
export default (window as any).LCSimulatorHost;
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"./src/", "../../index.ts"
|
||||
]
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,14 +11,18 @@ import {
|
|||
} from '@alilc/lowcode-shared';
|
||||
import { type ICodeScope, CodeScope } from './codeScope';
|
||||
import { mapValue } from './value';
|
||||
import { evaluate } from './evaluate';
|
||||
import { defaultSandbox } from './sandbox';
|
||||
|
||||
export interface ISandbox {
|
||||
eval(code: string, scope: any): any;
|
||||
}
|
||||
|
||||
export interface CodeRuntimeOptions<T extends StringDictionary = StringDictionary> {
|
||||
initScopeValue?: Partial<T>;
|
||||
|
||||
parentScope?: ICodeScope;
|
||||
|
||||
evalCodeFunction?: EvalCodeFunction;
|
||||
sandbox?: ISandbox;
|
||||
}
|
||||
|
||||
export interface ICodeRuntime<T extends StringDictionary = StringDictionary> extends IDisposable {
|
||||
|
@ -37,22 +41,20 @@ export interface ICodeRuntime<T extends StringDictionary = StringDictionary> ext
|
|||
|
||||
export type NodeResolverHandler = (node: JSNode) => JSNode | false | undefined;
|
||||
|
||||
export type EvalCodeFunction = (code: string, scope: any) => any;
|
||||
|
||||
export class CodeRuntime<T extends StringDictionary = StringDictionary>
|
||||
extends Disposable
|
||||
implements ICodeRuntime<T>
|
||||
{
|
||||
private _codeScope: ICodeScope<T>;
|
||||
|
||||
private _evalCodeFunction: EvalCodeFunction = evaluate;
|
||||
private _sandbox: ISandbox = defaultSandbox;
|
||||
|
||||
private _resolveHandlers: NodeResolverHandler[] = [];
|
||||
|
||||
constructor(options: CodeRuntimeOptions<T> = {}) {
|
||||
super();
|
||||
|
||||
if (options.evalCodeFunction) this._evalCodeFunction = options.evalCodeFunction;
|
||||
if (options.sandbox) this._sandbox = options.sandbox;
|
||||
this._codeScope = this._addDispose(
|
||||
options.parentScope
|
||||
? options.parentScope.createChild<T>(options.initScopeValue ?? {})
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { type EvalCodeFunction } from './codeRuntime';
|
||||
|
||||
export const evaluate: EvalCodeFunction = (code: string, scope: any) => {
|
||||
return new Function('scope', `"use strict";return (function(){return (${code})}).bind(scope)();`)(
|
||||
scope,
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { type ISandbox } from './codeRuntime';
|
||||
|
||||
export const defaultSandbox: ISandbox = {
|
||||
eval(code, scope) {
|
||||
return new Function(
|
||||
'scope',
|
||||
`"use strict";return (function(){return (${code})}).bind(scope)();`,
|
||||
)(scope);
|
||||
},
|
||||
};
|
|
@ -15,7 +15,7 @@ import {
|
|||
Disposable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { type ICodeRuntime } from '../code-runtime';
|
||||
import { IWidget, Widget } from '../widget';
|
||||
import { IWidget, Widget } from './widget';
|
||||
|
||||
export interface NormalizedComponentNode extends ComponentNode {
|
||||
loopArgs: [string, string];
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
export function illegalArgument(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal argument: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal argument');
|
||||
}
|
||||
}
|
|
@ -7,9 +7,6 @@ export * from './platform';
|
|||
export * from './logger';
|
||||
export * from './intl';
|
||||
export * from './instantiation';
|
||||
|
||||
export * from './keyCodes';
|
||||
export * from './errors';
|
||||
export * from './disposable';
|
||||
|
||||
export * from './linkedList';
|
||||
|
|
|
@ -52,8 +52,6 @@ export function mapDepsToBeanId(beanId: BeanIdentifier<any>, target: Constructor
|
|||
}
|
||||
}
|
||||
|
||||
export function getBeanDependecies(
|
||||
target: Constructor,
|
||||
): { id: BeanIdentifier<any>; index: number }[] {
|
||||
export function getBeanDependecies(target: Constructor): { beanId: BeanIdentifier<any>; index: number }[] {
|
||||
return (target as any)[DEPENDENCIES] || [];
|
||||
}
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import { dispose, isDisposable } from '../disposable';
|
||||
import { Graph, CyclicDependencyError } from '../graph';
|
||||
import {
|
||||
type BeanIdentifier,
|
||||
BeanContainer,
|
||||
type Constructor,
|
||||
getBeanDependecies,
|
||||
CtorDescriptor,
|
||||
} from './container';
|
||||
import { type BeanIdentifier, BeanContainer, type Constructor, getBeanDependecies, CtorDescriptor } from './container';
|
||||
import { createDecorator } from './decorators';
|
||||
|
||||
export interface InstanceAccessor {
|
||||
|
@ -16,10 +10,7 @@ export interface InstanceAccessor {
|
|||
export interface IInstantiationService {
|
||||
createInstance<T extends Constructor>(Ctor: T, ...args: any[]): InstanceType<T>;
|
||||
|
||||
invokeFunction<R, Args extends any[] = []>(
|
||||
fn: (accessor: InstanceAccessor, ...args: Args) => R,
|
||||
...args: Args
|
||||
): R;
|
||||
invokeFunction<R, Args extends any[] = []>(fn: (accessor: InstanceAccessor, ...args: Args) => R, ...args: Args): R;
|
||||
|
||||
createChild(container: BeanContainer): IInstantiationService;
|
||||
|
||||
|
@ -77,10 +68,7 @@ export class InstantiationService implements IInstantiationService {
|
|||
/**
|
||||
* Calls a function with a service accessor.
|
||||
*/
|
||||
invokeFunction<R, TS extends any[] = []>(
|
||||
fn: (accessor: InstanceAccessor, ...args: TS) => R,
|
||||
...args: TS
|
||||
): R {
|
||||
invokeFunction<R, TS extends any[] = []>(fn: (accessor: InstanceAccessor, ...args: TS) => R, ...args: TS): R {
|
||||
this._throwIfDisposed();
|
||||
|
||||
const accessor: InstanceAccessor = {
|
||||
|
@ -105,9 +93,9 @@ export class InstantiationService implements IInstantiationService {
|
|||
const beanArgs = [];
|
||||
|
||||
for (const dependency of beanDependencies) {
|
||||
const instance = this._getOrCreateInstance(dependency.id);
|
||||
const instance = this._getOrCreateInstance(dependency.beanId);
|
||||
if (!instance) {
|
||||
throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.id}.`);
|
||||
throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.beanId}.`);
|
||||
}
|
||||
|
||||
beanArgs.push(instance);
|
||||
|
@ -149,9 +137,7 @@ export class InstantiationService implements IInstantiationService {
|
|||
}
|
||||
|
||||
private _createAndCacheServiceInstance<T>(id: BeanIdentifier<T>, desc: CtorDescriptor<T>): T {
|
||||
const graph = new Graph<{ id: BeanIdentifier<T>; desc: CtorDescriptor<T> }>((data) =>
|
||||
data.id.toString(),
|
||||
);
|
||||
const graph = new Graph<{ id: BeanIdentifier<T>; desc: CtorDescriptor<T> }>((data) => data.id.toString());
|
||||
|
||||
let cycleCount = 0;
|
||||
const stack = [{ id, desc }];
|
||||
|
@ -174,16 +160,14 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
// check all dependencies for existence and if they need to be created first
|
||||
for (const dependency of getBeanDependecies(item.desc.ctor)) {
|
||||
const instanceOrDesc = this._container.get(dependency.id);
|
||||
const instanceOrDesc = this._container.get(dependency.beanId);
|
||||
if (!instanceOrDesc) {
|
||||
throw new Error(
|
||||
`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`,
|
||||
);
|
||||
throw new Error(`[createInstance] ${id} depends on ${dependency.beanId} which is NOT registered.`);
|
||||
}
|
||||
|
||||
if (instanceOrDesc instanceof CtorDescriptor) {
|
||||
const d = {
|
||||
id: dependency.id,
|
||||
id: dependency.beanId,
|
||||
desc: instanceOrDesc,
|
||||
};
|
||||
graph.insertEdge(item, d);
|
||||
|
@ -210,11 +194,7 @@ export class InstantiationService implements IInstantiationService {
|
|||
const instanceOrDesc = this._container.get(data.id);
|
||||
if (instanceOrDesc instanceof CtorDescriptor) {
|
||||
// create instance and overwrite the service collections
|
||||
const instance = this._createServiceInstanceWithOwner(
|
||||
data.id,
|
||||
data.desc.ctor,
|
||||
data.desc.staticArguments,
|
||||
);
|
||||
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments);
|
||||
this._setCreatedServiceInstance(data.id, instance);
|
||||
}
|
||||
graph.removeNode(data);
|
||||
|
|
|
@ -3,3 +3,11 @@ export function invariant(check: unknown, message: string, thing?: any): asserts
|
|||
throw new Error(`Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function illegalArgument(name?: string): Error {
|
||||
if (name) {
|
||||
return new Error(`Illegal argument: ${name}`);
|
||||
} else {
|
||||
return new Error('Illegal argument');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ async function run() {
|
|||
});
|
||||
|
||||
if (buildTypes) {
|
||||
await execa('pnpm', ['--filter', finalName[0], 'build:dts'], {
|
||||
await execa('pnpm', ['--filter', manifest.name, 'build:dts'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue