@fresh-editor/fresh-editor 0.1.96 → 0.1.98
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/CHANGELOG.md +28 -0
- package/package.json +1 -1
- package/plugins/lib/fresh.d.ts +4 -0
- package/plugins/pkg.ts +132 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## 0.1.98
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **File Explorer Quick Search**: Type to filter files/directories with fuzzy matching. ESC or Backspace clears the search (#892).
|
|
8
|
+
|
|
9
|
+
* **Sort Lines Command**: New command to alphabetically sort selected lines.
|
|
10
|
+
|
|
11
|
+
* **Paragraph Selection**: Ctrl+Shift+Up/Down extends selection to previous/next empty line.
|
|
12
|
+
|
|
13
|
+
* **Local Package Install**: Package manager now supports installing plugins/themes from local file paths (e.g., `/path/to/package`, `~/repos/plugin`).
|
|
14
|
+
|
|
15
|
+
* **Plugin API**: Added `setLineWrap` for plugins to control line wrapping.
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* Fixed data corruption when saving large files with in-place writes.
|
|
20
|
+
|
|
21
|
+
* Fixed UI hang when loading shortcuts in Open File dialog (#903).
|
|
22
|
+
|
|
23
|
+
* Fixed file explorer failing to open at root path "/" (#902).
|
|
24
|
+
|
|
25
|
+
* Fixed Settings UI search results not scrolling properly (#905).
|
|
26
|
+
|
|
27
|
+
* Fixed multi-cursor cut operations not batching undo correctly.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
3
31
|
## 0.1.96
|
|
4
32
|
|
|
5
33
|
### Features
|
package/package.json
CHANGED
package/plugins/lib/fresh.d.ts
CHANGED
|
@@ -1069,6 +1069,10 @@ interface EditorAPI {
|
|
|
1069
1069
|
*/
|
|
1070
1070
|
setLineNumbers(bufferId: number, enabled: boolean): boolean;
|
|
1071
1071
|
/**
|
|
1072
|
+
* Enable or disable line wrapping for a buffer/split
|
|
1073
|
+
*/
|
|
1074
|
+
setLineWrap(bufferId: number, splitId: number | null, enabled: boolean): boolean;
|
|
1075
|
+
/**
|
|
1072
1076
|
* Create a scroll sync group for anchor-based synchronized scrolling
|
|
1073
1077
|
*/
|
|
1074
1078
|
createScrollSyncGroup(groupId: number, leftSplit: number, rightSplit: number): boolean;
|
package/plugins/pkg.ts
CHANGED
|
@@ -155,12 +155,14 @@ interface Lockfile {
|
|
|
155
155
|
// =============================================================================
|
|
156
156
|
|
|
157
157
|
interface ParsedPackageUrl {
|
|
158
|
-
/** The base git repository URL (without fragment) */
|
|
158
|
+
/** The base git repository URL or local path (without fragment) */
|
|
159
159
|
repoUrl: string;
|
|
160
|
-
/** Optional path within the repository (from fragment) */
|
|
160
|
+
/** Optional path within the repository/directory (from fragment) */
|
|
161
161
|
subpath: string | null;
|
|
162
162
|
/** Extracted package name */
|
|
163
163
|
name: string;
|
|
164
|
+
/** Whether this is a local file path (not a remote URL) */
|
|
165
|
+
isLocal: boolean;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// =============================================================================
|
|
@@ -206,6 +208,29 @@ async function gitCommand(args: string[]): Promise<{ exit_code: number; stdout:
|
|
|
206
208
|
return result;
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Check if a string is a local file path (not a URL).
|
|
213
|
+
*/
|
|
214
|
+
function isLocalPath(str: string): boolean {
|
|
215
|
+
// Absolute paths start with /
|
|
216
|
+
if (str.startsWith("/")) return true;
|
|
217
|
+
// Windows absolute paths (C:\, D:\, etc.)
|
|
218
|
+
if (/^[A-Za-z]:[\\\/]/.test(str)) return true;
|
|
219
|
+
// Relative paths starting with . or ..
|
|
220
|
+
if (str.startsWith("./") || str.startsWith("../")) return true;
|
|
221
|
+
// Home directory expansion
|
|
222
|
+
if (str.startsWith("~/")) return true;
|
|
223
|
+
// Not a URL scheme (http://, https://, git://, ssh://, file://)
|
|
224
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(str)) {
|
|
225
|
+
// If it doesn't look like a URL and doesn't contain @, it's probably a path
|
|
226
|
+
// (git@github.com:user/repo is a git URL)
|
|
227
|
+
if (!str.includes("@") || str.startsWith("/")) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
/**
|
|
210
235
|
* Parse a package URL that may contain a subpath fragment.
|
|
211
236
|
*
|
|
@@ -213,6 +238,8 @@ async function gitCommand(args: string[]): Promise<{ exit_code: number; stdout:
|
|
|
213
238
|
* - `https://github.com/user/repo` - standard repo
|
|
214
239
|
* - `https://github.com/user/repo#path/to/plugin` - monorepo with subpath
|
|
215
240
|
* - `https://github.com/user/repo.git#packages/my-plugin` - with .git suffix
|
|
241
|
+
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
242
|
+
* - `/path/to/local/package` - direct local package path
|
|
216
243
|
*
|
|
217
244
|
* The fragment (after #) specifies a subdirectory within the repo.
|
|
218
245
|
*/
|
|
@@ -234,19 +261,22 @@ function parsePackageUrl(url: string): ParsedPackageUrl {
|
|
|
234
261
|
repoUrl = url;
|
|
235
262
|
}
|
|
236
263
|
|
|
264
|
+
// Determine if this is a local path
|
|
265
|
+
const isLocal = isLocalPath(repoUrl);
|
|
266
|
+
|
|
237
267
|
// Extract package name
|
|
238
268
|
let name: string;
|
|
239
269
|
if (subpath) {
|
|
240
|
-
// For monorepo, use the last component of the subpath
|
|
270
|
+
// For monorepo/directory, use the last component of the subpath
|
|
241
271
|
const parts = subpath.split("/");
|
|
242
272
|
name = parts[parts.length - 1].replace(/^fresh-/, "");
|
|
243
273
|
} else {
|
|
244
|
-
// For regular repo, use
|
|
274
|
+
// For regular repo/path, use the last component
|
|
245
275
|
const match = repoUrl.match(/\/([^\/]+?)(\.git)?$/);
|
|
246
276
|
name = match ? match[1].replace(/^fresh-/, "") : "unknown";
|
|
247
277
|
}
|
|
248
278
|
|
|
249
|
-
return { repoUrl, subpath, name };
|
|
279
|
+
return { repoUrl, subpath, name, isLocal };
|
|
250
280
|
}
|
|
251
281
|
|
|
252
282
|
/**
|
|
@@ -603,12 +633,15 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
|
|
|
603
633
|
}
|
|
604
634
|
|
|
605
635
|
/**
|
|
606
|
-
* Install a package from git URL.
|
|
636
|
+
* Install a package from git URL or local path.
|
|
607
637
|
*
|
|
608
|
-
* Supports
|
|
609
|
-
* - `https://github.com/user/repo
|
|
638
|
+
* Supports:
|
|
639
|
+
* - `https://github.com/user/repo` - standard git repo
|
|
640
|
+
* - `https://github.com/user/repo#packages/my-plugin` - monorepo with subpath
|
|
641
|
+
* - `/path/to/local/repo#subdir` - local path with subpath
|
|
642
|
+
* - `/path/to/local/package` - direct local package path
|
|
610
643
|
*
|
|
611
|
-
* For subpath packages, clones to temp directory and copies the subdirectory.
|
|
644
|
+
* For subpath packages, clones/copies to temp directory and copies the subdirectory.
|
|
612
645
|
*/
|
|
613
646
|
async function installPackage(
|
|
614
647
|
url: string,
|
|
@@ -632,11 +665,14 @@ async function installPackage(
|
|
|
632
665
|
|
|
633
666
|
editor.setStatus(`Installing ${packageName}...`);
|
|
634
667
|
|
|
635
|
-
if (parsed.
|
|
636
|
-
//
|
|
668
|
+
if (parsed.isLocal) {
|
|
669
|
+
// Local path installation: copy directly
|
|
670
|
+
return await installFromLocalPath(parsed, packageName, targetDir);
|
|
671
|
+
} else if (parsed.subpath) {
|
|
672
|
+
// Remote monorepo installation: clone to temp, copy subdirectory
|
|
637
673
|
return await installFromMonorepo(parsed, packageName, targetDir, version);
|
|
638
674
|
} else {
|
|
639
|
-
// Standard installation: clone directly
|
|
675
|
+
// Standard git installation: clone directly
|
|
640
676
|
return await installFromRepo(parsed.repoUrl, packageName, targetDir, version);
|
|
641
677
|
}
|
|
642
678
|
}
|
|
@@ -705,6 +741,90 @@ async function installFromRepo(
|
|
|
705
741
|
return true;
|
|
706
742
|
}
|
|
707
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Install from a local file path.
|
|
746
|
+
*
|
|
747
|
+
* Strategy:
|
|
748
|
+
* - If subpath is specified: copy that subdirectory
|
|
749
|
+
* - Otherwise: copy the entire directory
|
|
750
|
+
* - Store the source path for reference
|
|
751
|
+
*/
|
|
752
|
+
async function installFromLocalPath(
|
|
753
|
+
parsed: ParsedPackageUrl,
|
|
754
|
+
packageName: string,
|
|
755
|
+
targetDir: string
|
|
756
|
+
): Promise<boolean> {
|
|
757
|
+
// Resolve the full source path
|
|
758
|
+
let sourcePath = parsed.repoUrl;
|
|
759
|
+
|
|
760
|
+
// Handle home directory expansion
|
|
761
|
+
if (sourcePath.startsWith("~/")) {
|
|
762
|
+
const home = editor.getEnv("HOME") || editor.getEnv("USERPROFILE") || "";
|
|
763
|
+
sourcePath = editor.pathJoin(home, sourcePath.slice(2));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// If there's a subpath, append it
|
|
767
|
+
if (parsed.subpath) {
|
|
768
|
+
sourcePath = editor.pathJoin(sourcePath, parsed.subpath);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Check if source exists
|
|
772
|
+
if (!editor.fileExists(sourcePath)) {
|
|
773
|
+
editor.setStatus(`Local path not found: ${sourcePath}`);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Check if it's a directory (by checking for package.json)
|
|
778
|
+
const manifestPath = editor.pathJoin(sourcePath, "package.json");
|
|
779
|
+
if (!editor.fileExists(manifestPath)) {
|
|
780
|
+
editor.setStatus(`Not a valid package (no package.json): ${sourcePath}`);
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Copy the directory to target
|
|
785
|
+
editor.setStatus(`Copying from ${sourcePath}...`);
|
|
786
|
+
const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, targetDir]);
|
|
787
|
+
if (copyResult.exit_code !== 0) {
|
|
788
|
+
editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Validate package structure
|
|
793
|
+
const validation = validatePackage(targetDir, packageName);
|
|
794
|
+
if (!validation.valid) {
|
|
795
|
+
editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
|
|
796
|
+
editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
|
|
797
|
+
// Clean up the invalid package
|
|
798
|
+
await editor.spawnProcess("rm", ["-rf", targetDir]);
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Store the source path for reference
|
|
803
|
+
const sourceInfo = {
|
|
804
|
+
local_path: sourcePath,
|
|
805
|
+
original_url: parsed.subpath ? `${parsed.repoUrl}#${parsed.subpath}` : parsed.repoUrl,
|
|
806
|
+
installed_at: new Date().toISOString()
|
|
807
|
+
};
|
|
808
|
+
await writeJsonFile(editor.pathJoin(targetDir, ".fresh-source.json"), sourceInfo);
|
|
809
|
+
|
|
810
|
+
const manifest = validation.manifest;
|
|
811
|
+
|
|
812
|
+
// Dynamically load plugins, reload themes, or load language packs
|
|
813
|
+
if (manifest?.type === "plugin" && validation.entryPath) {
|
|
814
|
+
await editor.loadPlugin(validation.entryPath);
|
|
815
|
+
editor.setStatus(`Installed and activated ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
816
|
+
} else if (manifest?.type === "theme") {
|
|
817
|
+
editor.reloadThemes();
|
|
818
|
+
editor.setStatus(`Installed theme ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
819
|
+
} else if (manifest?.type === "language") {
|
|
820
|
+
await loadLanguagePack(targetDir, manifest);
|
|
821
|
+
editor.setStatus(`Installed language pack ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
822
|
+
} else {
|
|
823
|
+
editor.setStatus(`Installed ${packageName}${manifest ? ` v${manifest.version}` : ""}`);
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
|
|
708
828
|
/**
|
|
709
829
|
* Install from a monorepo (URL with subpath fragment)
|
|
710
830
|
*
|