feat: init docs by vitepress

This commit is contained in:
1ncounter 2024-08-08 17:39:16 +08:00
parent a7820908d7
commit 9e08c2a224
76 changed files with 2592 additions and 759 deletions

3
.gitignore vendored
View File

@ -103,4 +103,5 @@ typings/
codealike.json
.node
.must.config.js
# docs
.vitepress

View File

@ -1,5 +1,5 @@
{
"printWidth": 100,
"printWidth": 120,
"tabWidth": 2,
"semi": true,
"jsxSingleQuote": false,

View File

@ -12,3 +12,7 @@
7. github workflows
lodaes replace
IDE 的引擎设计
工作区的虚拟文件设计
窗口、视图、编辑器、边栏、面板的设计

49
docs/api-examples.md Normal file
View File

@ -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
docs/api/index.md Normal file
View File

View File

@ -0,0 +1,8 @@
# 你好,世界
step1: 启动引擎,新建一个项目,新建一个低代码文件(.lc)
step2: 打开画布
step3: 拖拽组件
step4: 点击保存 (cmd + s)
step5: 点击预览 (cmd + p)
step6: 点击关闭,重新打开这个页面,内容不变

View File

View File

22
docs/index.md Normal file
View File

@ -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: 配套生态开箱即用,打造企业级低代码技术体系
---

85
docs/markdown-examples.md Normal file
View File

@ -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).

23
docs/package.json Normal file
View File

@ -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"
}
}

1
docs/public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -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),
);

View File

@ -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 &nbsp; (no-break space) character.
* Unicode Character 'NO-BREAK SPACE' (U+00A0)
*/
NoBreakSpace = 160,
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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 {

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
export const http = 'http';
export const https = 'https';
export const file = 'file';
export const untitled = 'untitled';

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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';
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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`;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
export * from './file';
export * from './fileService';
export * from './inMemoryFileSystemProvider';

View File

@ -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';

View File

@ -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:

View File

@ -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() {

View File

@ -0,0 +1,2 @@
export * from './window';
export * from './windowService';

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -1 +1,2 @@
export * from './workbenchService';
export * from './workbench';

View File

@ -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>[] {

View File

@ -0,0 +1,5 @@
export const enum WorkbenchState {
EMPTY = 1,
FOLDER,
WORKSPACE /* preset */,
}

View File

@ -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;
}

View File

@ -1,6 +0,0 @@
/**
* URI -> file content
* URI -> file Stat
*/
export interface IFileManagement {}

View File

@ -1 +0,0 @@
export interface IFileService {}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export * from './workspaceService';
export * from './workspace';
export * from './workspaceFolder';

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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) });
}

View File

@ -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);
}
}

View File

@ -3,5 +3,5 @@
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
"include": ["src", "src/common/uri.ts"]
}

View File

@ -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 {

View File

@ -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() {

View File

@ -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();

View File

@ -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';

View File

@ -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>;

View File

@ -1,3 +0,0 @@
export * from './createComponent';
export * from './reactiveState';
export * from './elements';

View File

@ -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",

View File

@ -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',
};

View File

@ -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>
`;

View File

@ -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();
});
})

View File

@ -1,7 +0,0 @@
export const Text = ({
__tag,
content,
...props
}: any) => (<div {...props}>{content}</div>);
export const Page = (props: any) => (<div>{props.children}</div>);

View File

@ -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;

View File

@ -1,9 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/", "../../index.ts"
]
"outDir": "dist"
}
}

View File

@ -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 ?? {})

View File

@ -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,
);
};

View File

@ -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);
},
};

View File

@ -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];

View File

@ -1,7 +0,0 @@
export function illegalArgument(name?: string): Error {
if (name) {
return new Error(`Illegal argument: ${name}`);
} else {
return new Error('Illegal argument');
}
}

View File

@ -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';

View File

@ -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] || [];
}

View File

@ -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);

View File

@ -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');
}
}

View File

@ -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',
});
}