@fresh-editor/fresh-editor 0.1.97 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.1.97",
3
+ "version": "0.1.98",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 repo name
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 monorepo URLs with subpath fragments:
609
- * - `https://github.com/user/repo#packages/my-plugin`
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.subpath) {
636
- // Monorepo installation: clone to temp, copy subdirectory
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
  *