@aprovan/patchwork-vscode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +17 -0
- package/LICENSE +373 -0
- package/README.md +31 -0
- package/dist/extension.d.ts +6 -0
- package/dist/extension.js +1405 -0
- package/dist/extension.js.map +1 -0
- package/media/outline.png +0 -0
- package/media/outline.svg +70 -0
- package/media/patchwork.png +0 -0
- package/media/patchwork.svg +72 -0
- package/package.json +144 -0
- package/src/extension.ts +612 -0
- package/src/providers/PatchworkFileSystemProvider.ts +205 -0
- package/src/providers/PatchworkTreeProvider.ts +177 -0
- package/src/providers/PreviewPanelProvider.ts +536 -0
- package/src/services/EditService.ts +24 -0
- package/src/services/EmbeddedStitchery.ts +82 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as vscode from "vscode";
|
|
2
|
+
import type { VirtualFile, VirtualProject } from "@aprovan/patchwork-compiler";
|
|
3
|
+
|
|
4
|
+
interface ParsedPatchworkUri {
|
|
5
|
+
projectId: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PatchworkFileSystemProvider implements vscode.FileSystemProvider {
|
|
10
|
+
private readonly onDidChangeFileEmitter = new vscode.EventEmitter<
|
|
11
|
+
vscode.FileChangeEvent[]
|
|
12
|
+
>();
|
|
13
|
+
readonly onDidChangeFile = this.onDidChangeFileEmitter.event;
|
|
14
|
+
|
|
15
|
+
private readonly projects = new Map<string, VirtualProject>();
|
|
16
|
+
|
|
17
|
+
setProject(id: string, project: VirtualProject): void {
|
|
18
|
+
this.projects.set(id, project);
|
|
19
|
+
this.onDidChangeFileEmitter.fire([]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
clearProjects(): void {
|
|
23
|
+
this.projects.clear();
|
|
24
|
+
this.onDidChangeFileEmitter.fire([]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
readFile(uri: vscode.Uri): Uint8Array {
|
|
28
|
+
const { projectId, path } = this.parseUri(uri);
|
|
29
|
+
const project = this.getProject(projectId);
|
|
30
|
+
const file = project.files.get(path);
|
|
31
|
+
if (!file) throw vscode.FileSystemError.FileNotFound(uri);
|
|
32
|
+
return this.encodeFileContent(file);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
writeFile(
|
|
36
|
+
uri: vscode.Uri,
|
|
37
|
+
content: Uint8Array,
|
|
38
|
+
options: { create: boolean; overwrite: boolean },
|
|
39
|
+
): void {
|
|
40
|
+
const { projectId, path } = this.parseUri(uri);
|
|
41
|
+
const project = this.getProject(projectId);
|
|
42
|
+
const exists = project.files.has(path);
|
|
43
|
+
if (!exists && !options.create) {
|
|
44
|
+
throw vscode.FileSystemError.FileNotFound(uri);
|
|
45
|
+
}
|
|
46
|
+
if (exists && !options.overwrite) {
|
|
47
|
+
throw vscode.FileSystemError.FileExists(uri);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const file = this.decodeFileContent(path, content);
|
|
51
|
+
project.files.set(path, file);
|
|
52
|
+
this.onDidChangeFileEmitter.fire([
|
|
53
|
+
{
|
|
54
|
+
type: exists
|
|
55
|
+
? vscode.FileChangeType.Changed
|
|
56
|
+
: vscode.FileChangeType.Created,
|
|
57
|
+
uri,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stat(uri: vscode.Uri): vscode.FileStat {
|
|
63
|
+
const { projectId, path } = this.parseUri(uri);
|
|
64
|
+
const project = this.getProject(projectId);
|
|
65
|
+
|
|
66
|
+
if (!path) {
|
|
67
|
+
return {
|
|
68
|
+
type: vscode.FileType.Directory,
|
|
69
|
+
ctime: 0,
|
|
70
|
+
mtime: 0,
|
|
71
|
+
size: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const file = project.files.get(path);
|
|
76
|
+
if (file) {
|
|
77
|
+
return {
|
|
78
|
+
type: vscode.FileType.File,
|
|
79
|
+
ctime: 0,
|
|
80
|
+
mtime: 0,
|
|
81
|
+
size: this.encodeFileContent(file).byteLength,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.hasDirectory(project, path)) {
|
|
86
|
+
return {
|
|
87
|
+
type: vscode.FileType.Directory,
|
|
88
|
+
ctime: 0,
|
|
89
|
+
mtime: 0,
|
|
90
|
+
size: 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw vscode.FileSystemError.FileNotFound(uri);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
readDirectory(uri: vscode.Uri): [string, vscode.FileType][] {
|
|
98
|
+
const { projectId, path } = this.parseUri(uri);
|
|
99
|
+
const project = this.getProject(projectId);
|
|
100
|
+
return this.listDirectoryEntries(project, path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
createDirectory(): void {
|
|
104
|
+
throw vscode.FileSystemError.NoPermissions(
|
|
105
|
+
"Patchwork file system is read/write via file edits only.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
delete(): void {
|
|
110
|
+
throw vscode.FileSystemError.NoPermissions(
|
|
111
|
+
"Patchwork file deletion is not implemented yet.",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
rename(): void {
|
|
116
|
+
throw vscode.FileSystemError.NoPermissions(
|
|
117
|
+
"Patchwork file rename is not implemented yet.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
watch(): vscode.Disposable {
|
|
122
|
+
return new vscode.Disposable(() => undefined);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private parseUri(uri: vscode.Uri): ParsedPatchworkUri {
|
|
126
|
+
if (uri.scheme !== "patchwork") {
|
|
127
|
+
throw vscode.FileSystemError.Unavailable(
|
|
128
|
+
"Unsupported URI scheme for Patchwork provider.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const projectId = uri.authority;
|
|
132
|
+
if (!projectId) {
|
|
133
|
+
throw vscode.FileSystemError.FileNotFound(uri);
|
|
134
|
+
}
|
|
135
|
+
const path = uri.path.replace(/^\/+/, "");
|
|
136
|
+
return { projectId, path };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getProject(projectId: string): VirtualProject {
|
|
140
|
+
const project = this.projects.get(projectId);
|
|
141
|
+
if (!project) {
|
|
142
|
+
throw vscode.FileSystemError.FileNotFound(
|
|
143
|
+
vscode.Uri.parse(`patchwork://${projectId}`),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return project;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private encodeFileContent(file: VirtualFile): Uint8Array {
|
|
150
|
+
if (file.encoding === "base64") {
|
|
151
|
+
return Buffer.from(file.content, "base64");
|
|
152
|
+
}
|
|
153
|
+
return Buffer.from(file.content, "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private decodeFileContent(path: string, content: Uint8Array): VirtualFile {
|
|
157
|
+
const hasNull = content.some((byte) => byte === 0);
|
|
158
|
+
if (hasNull) {
|
|
159
|
+
return {
|
|
160
|
+
path,
|
|
161
|
+
content: Buffer.from(content).toString("base64"),
|
|
162
|
+
encoding: "base64",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
path,
|
|
168
|
+
content: Buffer.from(content).toString("utf8"),
|
|
169
|
+
encoding: "utf8",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private hasDirectory(project: VirtualProject, path: string): boolean {
|
|
174
|
+
const prefix = path.endsWith("/") ? path : `${path}/`;
|
|
175
|
+
for (const filePath of project.files.keys()) {
|
|
176
|
+
if (filePath.startsWith(prefix)) return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private listDirectoryEntries(
|
|
182
|
+
project: VirtualProject,
|
|
183
|
+
path: string,
|
|
184
|
+
): [string, vscode.FileType][] {
|
|
185
|
+
const prefix = path ? `${path.replace(/\/+$/, "")}/` : "";
|
|
186
|
+
const entries = new Map<string, vscode.FileType>();
|
|
187
|
+
|
|
188
|
+
for (const filePath of project.files.keys()) {
|
|
189
|
+
if (!filePath.startsWith(prefix)) continue;
|
|
190
|
+
const remainder = filePath.slice(prefix.length);
|
|
191
|
+
if (!remainder) continue;
|
|
192
|
+
const [segment, ...rest] = remainder.split("/");
|
|
193
|
+
if (!segment) continue;
|
|
194
|
+
if (rest.length === 0) {
|
|
195
|
+
entries.set(segment, vscode.FileType.File);
|
|
196
|
+
} else if (!entries.has(segment)) {
|
|
197
|
+
entries.set(segment, vscode.FileType.Directory);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Array.from(entries.entries()).sort((a, b) =>
|
|
202
|
+
a[0].localeCompare(b[0]),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as vscode from "vscode";
|
|
2
|
+
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
3
|
+
|
|
4
|
+
type TreeNodeKind = "empty" | "project" | "folder" | "file";
|
|
5
|
+
|
|
6
|
+
interface PatchworkTreeNode {
|
|
7
|
+
label: string;
|
|
8
|
+
kind: TreeNodeKind;
|
|
9
|
+
projectId?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
children?: PatchworkTreeNode[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PatchworkTreeItem extends vscode.TreeItem {
|
|
15
|
+
readonly kind: TreeNodeKind;
|
|
16
|
+
readonly projectId?: string;
|
|
17
|
+
readonly path?: string;
|
|
18
|
+
readonly children?: PatchworkTreeItem[];
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
node: PatchworkTreeNode,
|
|
22
|
+
collapsibleState: vscode.TreeItemCollapsibleState,
|
|
23
|
+
) {
|
|
24
|
+
super(node.label, collapsibleState);
|
|
25
|
+
this.kind = node.kind;
|
|
26
|
+
this.projectId = node.projectId;
|
|
27
|
+
this.path = node.path;
|
|
28
|
+
this.children = node.children?.map(
|
|
29
|
+
(child) =>
|
|
30
|
+
new PatchworkTreeItem(
|
|
31
|
+
child,
|
|
32
|
+
child.children && child.children.length > 0
|
|
33
|
+
? vscode.TreeItemCollapsibleState.Collapsed
|
|
34
|
+
: vscode.TreeItemCollapsibleState.None,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (this.kind === "folder" || this.kind === "project") {
|
|
39
|
+
this.iconPath = vscode.ThemeIcon.Folder;
|
|
40
|
+
} else if (this.kind === "file") {
|
|
41
|
+
this.iconPath = vscode.ThemeIcon.File;
|
|
42
|
+
if (this.projectId && this.path) {
|
|
43
|
+
this.command = {
|
|
44
|
+
command: "patchwork.openFile",
|
|
45
|
+
title: "Open Patchwork File",
|
|
46
|
+
arguments: [this.projectId, this.path],
|
|
47
|
+
};
|
|
48
|
+
this.tooltip = this.path;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class PatchworkTreeProvider
|
|
55
|
+
implements vscode.TreeDataProvider<PatchworkTreeItem>
|
|
56
|
+
{
|
|
57
|
+
private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter<
|
|
58
|
+
PatchworkTreeItem | undefined
|
|
59
|
+
>();
|
|
60
|
+
readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
|
|
61
|
+
|
|
62
|
+
private readonly projects = new Map<string, VirtualProject>();
|
|
63
|
+
|
|
64
|
+
setProject(id: string, project: VirtualProject): void {
|
|
65
|
+
this.projects.set(id, project);
|
|
66
|
+
this.onDidChangeTreeDataEmitter.fire(undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clearProjects(): void {
|
|
70
|
+
this.projects.clear();
|
|
71
|
+
this.onDidChangeTreeDataEmitter.fire(undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getTreeItem(element: PatchworkTreeItem): vscode.TreeItem {
|
|
75
|
+
return element;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getChildren(element?: PatchworkTreeItem): Thenable<PatchworkTreeItem[]> {
|
|
79
|
+
if (!element) {
|
|
80
|
+
if (this.projects.size === 0) {
|
|
81
|
+
return Promise.resolve([
|
|
82
|
+
new PatchworkTreeItem(
|
|
83
|
+
{ label: "No Patchwork projects", kind: "empty" },
|
|
84
|
+
vscode.TreeItemCollapsibleState.None,
|
|
85
|
+
),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const projectItems = Array.from(this.projects.values())
|
|
90
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
91
|
+
.map(
|
|
92
|
+
(project) =>
|
|
93
|
+
new PatchworkTreeItem(
|
|
94
|
+
{
|
|
95
|
+
label: project.id,
|
|
96
|
+
kind: "project",
|
|
97
|
+
projectId: project.id,
|
|
98
|
+
children: this.buildProjectNodes(project),
|
|
99
|
+
},
|
|
100
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return Promise.resolve(projectItems);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (element.children) {
|
|
108
|
+
return Promise.resolve(element.children);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Promise.resolve([]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private buildProjectNodes(project: VirtualProject): PatchworkTreeNode[] {
|
|
115
|
+
type InternalNode = {
|
|
116
|
+
label: string;
|
|
117
|
+
kind: TreeNodeKind;
|
|
118
|
+
projectId: string;
|
|
119
|
+
path: string;
|
|
120
|
+
children: Map<string, InternalNode>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const root: InternalNode = {
|
|
124
|
+
label: project.id,
|
|
125
|
+
kind: "project",
|
|
126
|
+
projectId: project.id,
|
|
127
|
+
path: "",
|
|
128
|
+
children: new Map(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
for (const [path] of project.files) {
|
|
132
|
+
const parts = path.split("/").filter(Boolean);
|
|
133
|
+
let current = root;
|
|
134
|
+
let currentPath = "";
|
|
135
|
+
|
|
136
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
137
|
+
const part = parts[index];
|
|
138
|
+
const isFile = index === parts.length - 1;
|
|
139
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
140
|
+
|
|
141
|
+
if (!current.children.has(part)) {
|
|
142
|
+
current.children.set(part, {
|
|
143
|
+
label: part,
|
|
144
|
+
kind: isFile ? "file" : "folder",
|
|
145
|
+
projectId: project.id,
|
|
146
|
+
path: currentPath,
|
|
147
|
+
children: new Map(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const next = current.children.get(part);
|
|
152
|
+
if (next) current = next;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const collectChildren = (
|
|
157
|
+
nodeMap: Map<string, InternalNode>,
|
|
158
|
+
): PatchworkTreeNode[] => {
|
|
159
|
+
const nodes = Array.from(nodeMap.values());
|
|
160
|
+
return nodes
|
|
161
|
+
.sort((a, b) => {
|
|
162
|
+
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
163
|
+
return a.label.localeCompare(b.label);
|
|
164
|
+
})
|
|
165
|
+
.map((node) => ({
|
|
166
|
+
label: node.label,
|
|
167
|
+
kind: node.kind,
|
|
168
|
+
projectId: node.projectId,
|
|
169
|
+
path: node.path,
|
|
170
|
+
children:
|
|
171
|
+
node.kind === "file" ? undefined : collectChildren(node.children),
|
|
172
|
+
}));
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return collectChildren(root.children);
|
|
176
|
+
}
|
|
177
|
+
}
|