@fresh-editor/fresh-editor 0.2.17 → 0.2.20

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/package.json +1 -1
  3. package/plugins/astro-lsp.ts +118 -0
  4. package/plugins/bash-lsp.ts +161 -0
  5. package/plugins/clojure-lsp.ts +125 -0
  6. package/plugins/cmake-lsp.ts +138 -0
  7. package/plugins/config-schema.json +275 -29
  8. package/plugins/dart-lsp.ts +144 -0
  9. package/plugins/diagnostics_panel.ts +4 -12
  10. package/plugins/diff_nav.i18n.json +128 -0
  11. package/plugins/diff_nav.ts +196 -0
  12. package/plugins/elixir-lsp.ts +120 -0
  13. package/plugins/erlang-lsp.ts +121 -0
  14. package/plugins/fsharp-lsp.ts +125 -0
  15. package/plugins/git_gutter.ts +5 -0
  16. package/plugins/gleam-lsp.ts +124 -0
  17. package/plugins/graphql-lsp.ts +139 -0
  18. package/plugins/haskell-lsp.ts +125 -0
  19. package/plugins/julia-lsp.ts +111 -0
  20. package/plugins/kotlin-lsp.ts +162 -0
  21. package/plugins/lib/finder.ts +19 -12
  22. package/plugins/lib/fresh.d.ts +30 -1
  23. package/plugins/lua-lsp.ts +161 -0
  24. package/plugins/nim-lsp.ts +118 -0
  25. package/plugins/nix-lsp.ts +125 -0
  26. package/plugins/nushell-lsp.ts +144 -0
  27. package/plugins/ocaml-lsp.ts +119 -0
  28. package/plugins/perl-lsp.ts +118 -0
  29. package/plugins/php-lsp.ts +165 -0
  30. package/plugins/pkg.ts +37 -76
  31. package/plugins/protobuf-lsp.ts +144 -0
  32. package/plugins/r-lsp.ts +118 -0
  33. package/plugins/ruby-lsp.ts +165 -0
  34. package/plugins/scala-lsp.ts +119 -0
  35. package/plugins/schemas/package.schema.json +437 -272
  36. package/plugins/schemas/theme.schema.json +18 -0
  37. package/plugins/solidity-lsp.ts +130 -0
  38. package/plugins/sql-lsp.ts +129 -0
  39. package/plugins/svelte-lsp.ts +119 -0
  40. package/plugins/swift-lsp.ts +120 -0
  41. package/plugins/tailwindcss-lsp.ts +119 -0
  42. package/plugins/terraform-lsp.ts +144 -0
  43. package/plugins/theme_editor.i18n.json +70 -14
  44. package/plugins/theme_editor.ts +71 -39
  45. package/plugins/toml-lsp.ts +162 -0
  46. package/plugins/typst-lsp.ts +165 -0
  47. package/plugins/vue-lsp.ts +118 -0
  48. package/plugins/yaml-lsp.ts +163 -0
package/plugins/pkg.ts CHANGED
@@ -231,14 +231,10 @@ interface ParsedPackageUrl {
231
231
  // =============================================================================
232
232
 
233
233
  /**
234
- * Ensure a directory exists
234
+ * Ensure a directory exists (cross-platform)
235
235
  */
236
- async function ensureDir(path: string): Promise<boolean> {
237
- if (editor.fileExists(path)) {
238
- return true;
239
- }
240
- const result = await editor.spawnProcess("mkdir", ["-p", path]);
241
- return result.exit_code === 0;
236
+ function ensureDir(path: string): boolean {
237
+ return editor.createDir(path);
242
238
  }
243
239
 
244
240
  /**
@@ -395,7 +391,7 @@ async function writeJsonFile(path: string, data: unknown): Promise<boolean> {
395
391
  async function syncRegistry(): Promise<void> {
396
392
  editor.setStatus("Syncing package registry...");
397
393
 
398
- await ensureDir(INDEX_DIR);
394
+ ensureDir(INDEX_DIR);
399
395
 
400
396
  const sources = getRegistrySources();
401
397
  let synced = 0;
@@ -493,7 +489,7 @@ function loadRegistry(type: "plugins" | "themes" | "languages"): RegistryData {
493
489
  * Cache registry data locally for offline/fast access
494
490
  */
495
491
  async function cacheRegistry(): Promise<void> {
496
- await ensureDir(CACHE_DIR);
492
+ ensureDir(CACHE_DIR);
497
493
  const sources = getRegistrySources();
498
494
 
499
495
  for (const source of sources) {
@@ -823,7 +819,7 @@ async function installFromRepo(
823
819
  version?: string
824
820
  ): Promise<boolean> {
825
821
  // Clone to temp directory first to detect package type
826
- const tempDir = `/tmp/fresh-pkg-clone-${hashString(repoUrl)}-${Date.now()}`;
822
+ const tempDir = editor.pathJoin(editor.getTempDir(), `fresh-pkg-clone-${hashString(repoUrl)}-${Date.now()}`);
827
823
 
828
824
  const cloneArgs = ["clone"];
829
825
  if (!version || version === "latest") {
@@ -857,7 +853,7 @@ async function installFromRepo(
857
853
  editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
858
854
  editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
859
855
  // Clean up
860
- await editor.spawnProcess("rm", ["-rf", tempDir]);
856
+ editor.removePath(tempDir);
861
857
  return false;
862
858
  }
863
859
 
@@ -877,16 +873,15 @@ async function installFromRepo(
877
873
  // Check if already installed in correct location
878
874
  if (editor.fileExists(correctTargetDir)) {
879
875
  editor.setStatus(`Package '${packageName}' is already installed`);
880
- await editor.spawnProcess("rm", ["-rf", tempDir]);
876
+ editor.removePath(tempDir);
881
877
  return false;
882
878
  }
883
879
 
884
880
  // Ensure correct directory exists and move from temp
885
- await ensureDir(correctPackagesDir);
886
- const moveResult = await editor.spawnProcess("mv", [tempDir, correctTargetDir]);
887
- if (moveResult.exit_code !== 0) {
888
- editor.setStatus(`Failed to install ${packageName}: ${moveResult.stderr}`);
889
- await editor.spawnProcess("rm", ["-rf", tempDir]);
881
+ ensureDir(correctPackagesDir);
882
+ if (!editor.renamePath(tempDir, correctTargetDir)) {
883
+ editor.setStatus(`Failed to install ${packageName}: could not move package to target directory`);
884
+ editor.removePath(tempDir);
890
885
  return false;
891
886
  }
892
887
 
@@ -976,13 +971,12 @@ async function installFromLocalPath(
976
971
  }
977
972
 
978
973
  // Ensure correct directory exists
979
- await ensureDir(correctPackagesDir);
974
+ ensureDir(correctPackagesDir);
980
975
 
981
976
  // Copy the directory to correct target
982
977
  editor.setStatus(`Copying from ${sourcePath}...`);
983
- const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, correctTargetDir]);
984
- if (copyResult.exit_code !== 0) {
985
- editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
978
+ if (!editor.copyPath(sourcePath, correctTargetDir)) {
979
+ editor.setStatus(`Failed to copy package from ${sourcePath}`);
986
980
  return false;
987
981
  }
988
982
 
@@ -992,7 +986,7 @@ async function installFromLocalPath(
992
986
  editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
993
987
  editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
994
988
  // Clean up the invalid package
995
- await editor.spawnProcess("rm", ["-rf", correctTargetDir]);
989
+ editor.removePath(correctTargetDir);
996
990
  return false;
997
991
  }
998
992
 
@@ -1037,7 +1031,7 @@ async function installFromMonorepo(
1037
1031
  packageName: string,
1038
1032
  version?: string
1039
1033
  ): Promise<boolean> {
1040
- const tempDir = `/tmp/fresh-pkg-${hashString(parsed.repoUrl)}-${Date.now()}`;
1034
+ const tempDir = editor.pathJoin(editor.getTempDir(), `fresh-pkg-${hashString(parsed.repoUrl)}-${Date.now()}`);
1041
1035
 
1042
1036
  try {
1043
1037
  // Clone the full repo to temp
@@ -1068,7 +1062,7 @@ async function installFromMonorepo(
1068
1062
  const subpathDir = editor.pathJoin(tempDir, parsed.subpath!);
1069
1063
  if (!editor.fileExists(subpathDir)) {
1070
1064
  editor.setStatus(`Subpath '${parsed.subpath}' not found in repository`);
1071
- await editor.spawnProcess("rm", ["-rf", tempDir]);
1065
+ editor.removePath(tempDir);
1072
1066
  return false;
1073
1067
  }
1074
1068
 
@@ -1077,7 +1071,7 @@ async function installFromMonorepo(
1077
1071
  if (!validation.valid) {
1078
1072
  editor.warn(`[pkg] Invalid package '${packageName}': ${validation.error}`);
1079
1073
  editor.setStatus(`Failed to install ${packageName}: ${validation.error}`);
1080
- await editor.spawnProcess("rm", ["-rf", tempDir]);
1074
+ editor.removePath(tempDir);
1081
1075
  return false;
1082
1076
  }
1083
1077
 
@@ -1097,19 +1091,18 @@ async function installFromMonorepo(
1097
1091
  // Check if already installed
1098
1092
  if (editor.fileExists(correctTargetDir)) {
1099
1093
  editor.setStatus(`Package '${packageName}' is already installed`);
1100
- await editor.spawnProcess("rm", ["-rf", tempDir]);
1094
+ editor.removePath(tempDir);
1101
1095
  return false;
1102
1096
  }
1103
1097
 
1104
1098
  // Ensure correct directory exists
1105
- await ensureDir(correctPackagesDir);
1099
+ ensureDir(correctPackagesDir);
1106
1100
 
1107
1101
  // Copy subdirectory to correct target
1108
1102
  editor.setStatus(`Installing ${packageName} from ${parsed.subpath}...`);
1109
- const copyResult = await editor.spawnProcess("cp", ["-r", subpathDir, correctTargetDir]);
1110
- if (copyResult.exit_code !== 0) {
1111
- editor.setStatus(`Failed to copy package: ${copyResult.stderr}`);
1112
- await editor.spawnProcess("rm", ["-rf", tempDir]);
1103
+ if (!editor.copyPath(subpathDir, correctTargetDir)) {
1104
+ editor.setStatus(`Failed to copy package from ${parsed.subpath}`);
1105
+ editor.removePath(tempDir);
1113
1106
  return false;
1114
1107
  }
1115
1108
 
@@ -1143,7 +1136,7 @@ async function installFromMonorepo(
1143
1136
  return true;
1144
1137
  } finally {
1145
1138
  // Cleanup temp directory
1146
- await editor.spawnProcess("rm", ["-rf", tempDir]);
1139
+ editor.removePath(tempDir);
1147
1140
  }
1148
1141
  }
1149
1142
 
@@ -1408,16 +1401,14 @@ async function reinstallPackage(pkg: InstalledPackage): Promise<boolean> {
1408
1401
  }
1409
1402
 
1410
1403
  // Remove old copy
1411
- const rmResult = await editor.spawnProcess("rm", ["-rf", pkg.path]);
1412
- if (rmResult.exit_code !== 0) {
1413
- editor.setStatus(`Failed to remove old copy: ${rmResult.stderr}`);
1404
+ if (!editor.removePath(pkg.path)) {
1405
+ editor.setStatus(`Failed to remove old copy of ${pkg.name}`);
1414
1406
  return false;
1415
1407
  }
1416
1408
 
1417
1409
  // Re-copy from source
1418
- const copyResult = await editor.spawnProcess("cp", ["-r", sourcePath, pkg.path]);
1419
- if (copyResult.exit_code !== 0) {
1420
- editor.setStatus(`Failed to copy from source: ${copyResult.stderr}`);
1410
+ if (!editor.copyPath(sourcePath, pkg.path)) {
1411
+ editor.setStatus(`Failed to copy from source: ${sourcePath}`);
1421
1412
  return false;
1422
1413
  }
1423
1414
 
@@ -1474,13 +1465,8 @@ async function removePackage(pkg: InstalledPackage): Promise<boolean> {
1474
1465
  }
1475
1466
  }
1476
1467
 
1477
- // Use trash if available, otherwise rm -rf
1478
- let result = await editor.spawnProcess("trash", [pkg.path]);
1479
- if (result.exit_code !== 0) {
1480
- result = await editor.spawnProcess("rm", ["-rf", pkg.path]);
1481
- }
1482
-
1483
- if (result.exit_code === 0) {
1468
+ // Remove package directory
1469
+ if (editor.removePath(pkg.path)) {
1484
1470
  // Reload themes if we removed a theme so Select Theme list is updated
1485
1471
  if (pkg.type === "theme") {
1486
1472
  editor.reloadThemes();
@@ -1488,7 +1474,7 @@ async function removePackage(pkg: InstalledPackage): Promise<boolean> {
1488
1474
  editor.setStatus(`Removed ${pkg.name}`);
1489
1475
  return true;
1490
1476
  } else {
1491
- editor.setStatus(`Failed to remove ${pkg.name}: ${result.stderr}`);
1477
+ editor.setStatus(`Failed to remove ${pkg.name}`);
1492
1478
  return false;
1493
1479
  }
1494
1480
  }
@@ -1608,7 +1594,7 @@ async function installFromLockfile(): Promise<void> {
1608
1594
  }
1609
1595
  } else {
1610
1596
  // Need to clone
1611
- await ensureDir(PACKAGES_DIR);
1597
+ ensureDir(PACKAGES_DIR);
1612
1598
  const result = await gitCommand(["clone", `${entry.source}`, `${pluginPath}`]);
1613
1599
 
1614
1600
  if (result.exit_code === 0) {
@@ -3049,34 +3035,9 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
3049
3035
  // Note: Other commands (install_plugin, install_theme, update, remove, sync, etc.)
3050
3036
  // are available via the package manager UI and don't need global command palette entries.
3051
3037
 
3052
- // =============================================================================
3053
- // Startup: Load installed language packs and bundles
3054
- // =============================================================================
3055
-
3056
- (async function loadInstalledPackages() {
3057
- // Load language packs
3058
- const languages = getInstalledPackages("language");
3059
- for (const pkg of languages) {
3060
- if (pkg.manifest) {
3061
- editor.debug(`[pkg] Loading language pack: ${pkg.name}`);
3062
- await loadLanguagePack(pkg.path, pkg.manifest);
3063
- }
3064
- }
3065
- if (languages.length > 0) {
3066
- editor.debug(`[pkg] Loaded ${languages.length} language pack(s)`);
3067
- }
3068
-
3069
- // Load bundles
3070
- const bundles = getInstalledPackages("bundle");
3071
- for (const pkg of bundles) {
3072
- if (pkg.manifest) {
3073
- editor.debug(`[pkg] Loading bundle: ${pkg.name}`);
3074
- await loadBundle(pkg.path, pkg.manifest);
3075
- }
3076
- }
3077
- if (bundles.length > 0) {
3078
- editor.debug(`[pkg] Loaded ${bundles.length} bundle(s)`);
3079
- }
3080
- })();
3038
+ // Note: Startup loading of installed language packs and bundles is now handled
3039
+ // by Rust (services::packages::scan_installed_packages) during editor init.
3040
+ // The loadLanguagePack() and loadBundle() functions above are still used for
3041
+ // runtime installs via the package manager UI.
3081
3042
 
3082
3043
  editor.debug("Package Manager plugin loaded");
@@ -0,0 +1,144 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * Protobuf LSP Helper Plugin
6
+ *
7
+ * Provides user-friendly error handling for Protobuf LSP server issues.
8
+ * When buf fails to start, this plugin shows an actionable
9
+ * popup with installation instructions.
10
+ *
11
+ * Features:
12
+ * - Detects Protobuf LSP server errors (buf)
13
+ * - Shows popup with install commands (brew, npm, etc.)
14
+ * - Provides option to disable Protobuf LSP
15
+ *
16
+ * VS Code: "Buf" extension (auto-installs buf CLI), "Protobuf support" by peterj
17
+ * Neovim: nvim-lspconfig bufls
18
+ * Alternative: protols (open-source, tree-sitter based, standalone)
19
+ */
20
+
21
+ interface LspServerErrorData {
22
+ language: string;
23
+ server_command: string;
24
+ error_type: string;
25
+ message: string;
26
+ }
27
+
28
+ interface LspStatusClickedData {
29
+ language: string;
30
+ has_error: boolean;
31
+ }
32
+
33
+ interface ActionPopupResultData {
34
+ popup_id: string;
35
+ action_id: string;
36
+ }
37
+
38
+ // Install commands for Protobuf LSP server (buf)
39
+ // See: https://buf.build/docs/installation
40
+ const INSTALL_COMMANDS = {
41
+ brew: "brew install bufbuild/buf/buf",
42
+ npm: "npm i -g @bufbuild/buf",
43
+ go: "go install github.com/bufbuild/buf/cmd/buf@latest",
44
+ };
45
+
46
+ // Track error state for Protobuf LSP
47
+ let protobufLspError: { serverCommand: string; message: string } | null = null;
48
+
49
+ /**
50
+ * Handle LSP server errors for Protobuf
51
+ */
52
+ function on_protobuf_lsp_server_error(data: LspServerErrorData): void {
53
+ if (data.language !== "protobuf") {
54
+ return;
55
+ }
56
+
57
+ editor.debug(`protobuf-lsp: Server error - ${data.error_type}: ${data.message}`);
58
+
59
+ protobufLspError = {
60
+ serverCommand: data.server_command,
61
+ message: data.message,
62
+ };
63
+
64
+ if (data.error_type === "not_found") {
65
+ editor.setStatus(
66
+ `Protobuf LSP server '${data.server_command}' not found. Click status bar for help.`
67
+ );
68
+ } else {
69
+ editor.setStatus(`Protobuf LSP error: ${data.message}`);
70
+ }
71
+ }
72
+ registerHandler("on_protobuf_lsp_server_error", on_protobuf_lsp_server_error);
73
+ editor.on("lsp_server_error", "on_protobuf_lsp_server_error");
74
+
75
+ /**
76
+ * Handle status bar click when there's a Protobuf LSP error
77
+ */
78
+ function on_protobuf_lsp_status_clicked(data: LspStatusClickedData): void {
79
+ if (data.language !== "protobuf" || !protobufLspError) {
80
+ return;
81
+ }
82
+
83
+ editor.debug("protobuf-lsp: Status clicked, showing help popup");
84
+
85
+ editor.showActionPopup({
86
+ id: "protobuf-lsp-help",
87
+ title: "Protobuf Language Server Not Found",
88
+ message: `"${protobufLspError.serverCommand}" (Buf CLI) provides code completion, diagnostics, linting, and formatting for Protocol Buffer files. Copy a command below to install it, or visit https://buf.build/docs/installation for details.`,
89
+ actions: [
90
+ { id: "copy_brew", label: `Copy: ${INSTALL_COMMANDS.brew}` },
91
+ { id: "copy_npm", label: `Copy: ${INSTALL_COMMANDS.npm}` },
92
+ { id: "copy_go", label: `Copy: ${INSTALL_COMMANDS.go}` },
93
+ { id: "disable", label: "Disable Protobuf LSP" },
94
+ { id: "dismiss", label: "Dismiss (ESC)" },
95
+ ],
96
+ });
97
+ }
98
+ registerHandler("on_protobuf_lsp_status_clicked", on_protobuf_lsp_status_clicked);
99
+ editor.on("lsp_status_clicked", "on_protobuf_lsp_status_clicked");
100
+
101
+ /**
102
+ * Handle action popup results for Protobuf LSP help
103
+ */
104
+ function on_protobuf_lsp_action_result(data: ActionPopupResultData): void {
105
+ if (data.popup_id !== "protobuf-lsp-help") {
106
+ return;
107
+ }
108
+
109
+ editor.debug(`protobuf-lsp: Action selected - ${data.action_id}`);
110
+
111
+ switch (data.action_id) {
112
+ case "copy_brew":
113
+ editor.setClipboard(INSTALL_COMMANDS.brew);
114
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.brew);
115
+ break;
116
+
117
+ case "copy_npm":
118
+ editor.setClipboard(INSTALL_COMMANDS.npm);
119
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.npm);
120
+ break;
121
+
122
+ case "copy_go":
123
+ editor.setClipboard(INSTALL_COMMANDS.go);
124
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.go);
125
+ break;
126
+
127
+ case "disable":
128
+ editor.disableLspForLanguage("protobuf");
129
+ editor.setStatus("Protobuf LSP disabled");
130
+ protobufLspError = null;
131
+ break;
132
+
133
+ case "dismiss":
134
+ case "dismissed":
135
+ break;
136
+
137
+ default:
138
+ editor.debug(`protobuf-lsp: Unknown action: ${data.action_id}`);
139
+ }
140
+ }
141
+ registerHandler("on_protobuf_lsp_action_result", on_protobuf_lsp_action_result);
142
+ editor.on("action_popup_result", "on_protobuf_lsp_action_result");
143
+
144
+ editor.debug("protobuf-lsp: Plugin loaded");
@@ -0,0 +1,118 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * R LSP Helper Plugin
6
+ *
7
+ * Server: languageserver (R package)
8
+ * VS Code: "R" extension by REditorSupport (uses languageserver)
9
+ * Neovim: nvim-lspconfig r_language_server
10
+ * Install via: R's install.packages() - runs as an R script
11
+ * Note: Also install httpgd for plot viewer support
12
+ */
13
+
14
+ interface LspServerErrorData {
15
+ language: string;
16
+ server_command: string;
17
+ error_type: string;
18
+ message: string;
19
+ }
20
+
21
+ interface LspStatusClickedData {
22
+ language: string;
23
+ has_error: boolean;
24
+ }
25
+
26
+ interface ActionPopupResultData {
27
+ popup_id: string;
28
+ action_id: string;
29
+ }
30
+
31
+ const INSTALL_COMMANDS = {
32
+ r: 'R -e \'install.packages("languageserver")\'',
33
+ conda: "conda install -c conda-forge r-languageserver",
34
+ };
35
+
36
+ let rLspError: { serverCommand: string; message: string } | null = null;
37
+
38
+ function on_r_lsp_server_error(data: LspServerErrorData): void {
39
+ if (data.language !== "r") {
40
+ return;
41
+ }
42
+
43
+ editor.debug(`r-lsp: Server error - ${data.error_type}: ${data.message}`);
44
+
45
+ rLspError = {
46
+ serverCommand: data.server_command,
47
+ message: data.message,
48
+ };
49
+
50
+ if (data.error_type === "not_found") {
51
+ editor.setStatus(
52
+ `R LSP server '${data.server_command}' not found. Click status bar for help.`
53
+ );
54
+ } else {
55
+ editor.setStatus(`R LSP error: ${data.message}`);
56
+ }
57
+ }
58
+ registerHandler("on_r_lsp_server_error", on_r_lsp_server_error);
59
+ editor.on("lsp_server_error", "on_r_lsp_server_error");
60
+
61
+ function on_r_lsp_status_clicked(data: LspStatusClickedData): void {
62
+ if (data.language !== "r" || !rLspError) {
63
+ return;
64
+ }
65
+
66
+ editor.debug("r-lsp: Status clicked, showing help popup");
67
+
68
+ editor.showActionPopup({
69
+ id: "r-lsp-help",
70
+ title: "R Language Server Not Found",
71
+ message: `The R language server provides completion, diagnostics, hover, formatting, and go-to-definition for R files. It runs as an R script, so R must be installed.\n\nInstall the languageserver R package, then the server runs via: R --vanilla -e 'languageserver::run()'\nVS Code users: Install the "R" extension by REditorSupport.\nSee: https://github.com/REditorSupport/languageserver`,
72
+ actions: [
73
+ { id: "copy_r", label: `Copy: ${INSTALL_COMMANDS.r}` },
74
+ { id: "copy_conda", label: `Copy: ${INSTALL_COMMANDS.conda}` },
75
+ { id: "disable", label: "Disable R LSP" },
76
+ { id: "dismiss", label: "Dismiss (ESC)" },
77
+ ],
78
+ });
79
+ }
80
+ registerHandler("on_r_lsp_status_clicked", on_r_lsp_status_clicked);
81
+ editor.on("lsp_status_clicked", "on_r_lsp_status_clicked");
82
+
83
+ function on_r_lsp_action_result(data: ActionPopupResultData): void {
84
+ if (data.popup_id !== "r-lsp-help") {
85
+ return;
86
+ }
87
+
88
+ editor.debug(`r-lsp: Action selected - ${data.action_id}`);
89
+
90
+ switch (data.action_id) {
91
+ case "copy_r":
92
+ editor.setClipboard(INSTALL_COMMANDS.r);
93
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.r);
94
+ break;
95
+
96
+ case "copy_conda":
97
+ editor.setClipboard(INSTALL_COMMANDS.conda);
98
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.conda);
99
+ break;
100
+
101
+ case "disable":
102
+ editor.disableLspForLanguage("r");
103
+ editor.setStatus("R LSP disabled");
104
+ rLspError = null;
105
+ break;
106
+
107
+ case "dismiss":
108
+ case "dismissed":
109
+ break;
110
+
111
+ default:
112
+ editor.debug(`r-lsp: Unknown action: ${data.action_id}`);
113
+ }
114
+ }
115
+ registerHandler("on_r_lsp_action_result", on_r_lsp_action_result);
116
+ editor.on("action_popup_result", "on_r_lsp_action_result");
117
+
118
+ editor.debug("r-lsp: Plugin loaded");
@@ -0,0 +1,165 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * Ruby LSP Helper Plugin
6
+ *
7
+ * Provides user-friendly error handling for Ruby LSP server issues.
8
+ * When solargraph fails to start, this plugin shows an actionable
9
+ * popup with installation instructions.
10
+ *
11
+ * Features:
12
+ * - Detects Ruby LSP server errors (solargraph)
13
+ * - Shows popup with install commands (gem)
14
+ * - Allows copying install commands to clipboard
15
+ * - Provides option to disable Ruby LSP
16
+ *
17
+ * Alternatives:
18
+ * - ruby-lsp (Shopify): Modern Ruby LSP - gem install ruby-lsp (https://github.com/Shopify/ruby-lsp)
19
+ * - Steep: Ruby type checker with LSP support (https://github.com/soutaro/steep)
20
+ * - Sorbet: Gradual type checker for Ruby (https://sorbet.org/)
21
+ */
22
+
23
+ interface LspServerErrorData {
24
+ language: string;
25
+ server_command: string;
26
+ error_type: string;
27
+ message: string;
28
+ }
29
+
30
+ interface LspStatusClickedData {
31
+ language: string;
32
+ has_error: boolean;
33
+ }
34
+
35
+ interface ActionPopupResultData {
36
+ popup_id: string;
37
+ action_id: string;
38
+ }
39
+
40
+ // Install commands for Ruby LSP server (solargraph)
41
+ // See: https://solargraph.org/guides/getting-started
42
+ const INSTALL_COMMANDS = {
43
+ gem: "gem install solargraph",
44
+ bundler: "bundle add solargraph --group development",
45
+ };
46
+
47
+ // Alternative LSP server
48
+ const ALT_INSTALL = "gem install ruby-lsp";
49
+
50
+ // Track error state for Ruby LSP
51
+ let rubyLspError: { serverCommand: string; message: string } | null = null;
52
+
53
+ /**
54
+ * Handle LSP server errors for Ruby
55
+ */
56
+ function on_ruby_lsp_server_error(data: LspServerErrorData): void {
57
+ // Only handle Ruby language errors
58
+ if (data.language !== "ruby") {
59
+ return;
60
+ }
61
+
62
+ editor.debug(`ruby-lsp: Server error - ${data.error_type}: ${data.message}`);
63
+
64
+ // Store error state for later reference
65
+ rubyLspError = {
66
+ serverCommand: data.server_command,
67
+ message: data.message,
68
+ };
69
+
70
+ // Show a status message for immediate feedback
71
+ if (data.error_type === "not_found") {
72
+ editor.setStatus(
73
+ `Ruby LSP server '${data.server_command}' not found. Click status bar for help.`
74
+ );
75
+ } else {
76
+ editor.setStatus(`Ruby LSP error: ${data.message}`);
77
+ }
78
+ }
79
+ registerHandler("on_ruby_lsp_server_error", on_ruby_lsp_server_error);
80
+
81
+ // Register hook for LSP server errors
82
+ editor.on("lsp_server_error", "on_ruby_lsp_server_error");
83
+
84
+ /**
85
+ * Handle status bar click when there's a Ruby LSP error
86
+ */
87
+ function on_ruby_lsp_status_clicked(
88
+ data: LspStatusClickedData
89
+ ): void {
90
+ // Only handle Ruby language clicks when there's an error
91
+ if (data.language !== "ruby" || !rubyLspError) {
92
+ return;
93
+ }
94
+
95
+ editor.debug("ruby-lsp: Status clicked, showing help popup");
96
+
97
+ // Show action popup with install options
98
+ editor.showActionPopup({
99
+ id: "ruby-lsp-help",
100
+ title: "Ruby Language Server Not Found",
101
+ message: `"${rubyLspError.serverCommand}" provides code completion, diagnostics, and navigation for Ruby files. Requires Ruby/RubyGems. Copy a command below to install it, or visit https://solargraph.org/guides/getting-started for details. Alternative: Shopify's ruby-lsp (https://github.com/Shopify/ruby-lsp).`,
102
+ actions: [
103
+ { id: "copy_gem", label: `Copy: ${INSTALL_COMMANDS.gem}` },
104
+ { id: "copy_bundler", label: `Copy: ${INSTALL_COMMANDS.bundler}` },
105
+ { id: "copy_alt", label: `Alternative: ${ALT_INSTALL}` },
106
+ { id: "disable", label: "Disable Ruby LSP" },
107
+ { id: "dismiss", label: "Dismiss (ESC)" },
108
+ ],
109
+ });
110
+ }
111
+ registerHandler("on_ruby_lsp_status_clicked", on_ruby_lsp_status_clicked);
112
+
113
+ // Register hook for status bar clicks
114
+ editor.on("lsp_status_clicked", "on_ruby_lsp_status_clicked");
115
+
116
+ /**
117
+ * Handle action popup results for Ruby LSP help
118
+ */
119
+ function on_ruby_lsp_action_result(
120
+ data: ActionPopupResultData
121
+ ): void {
122
+ // Only handle our popup
123
+ if (data.popup_id !== "ruby-lsp-help") {
124
+ return;
125
+ }
126
+
127
+ editor.debug(`ruby-lsp: Action selected - ${data.action_id}`);
128
+
129
+ switch (data.action_id) {
130
+ case "copy_gem":
131
+ editor.setClipboard(INSTALL_COMMANDS.gem);
132
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.gem);
133
+ break;
134
+
135
+ case "copy_bundler":
136
+ editor.setClipboard(INSTALL_COMMANDS.bundler);
137
+ editor.setStatus("Copied: " + INSTALL_COMMANDS.bundler);
138
+ break;
139
+
140
+ case "copy_alt":
141
+ editor.setClipboard(ALT_INSTALL);
142
+ editor.setStatus("Copied: " + ALT_INSTALL);
143
+ break;
144
+
145
+ case "disable":
146
+ editor.disableLspForLanguage("ruby");
147
+ editor.setStatus("Ruby LSP disabled");
148
+ rubyLspError = null;
149
+ break;
150
+
151
+ case "dismiss":
152
+ case "dismissed":
153
+ // Just close the popup without action
154
+ break;
155
+
156
+ default:
157
+ editor.debug(`ruby-lsp: Unknown action: ${data.action_id}`);
158
+ }
159
+ }
160
+ registerHandler("on_ruby_lsp_action_result", on_ruby_lsp_action_result);
161
+
162
+ // Register hook for action popup results
163
+ editor.on("action_popup_result", "on_ruby_lsp_action_result");
164
+
165
+ editor.debug("ruby-lsp: Plugin loaded");