@fresh-editor/fresh-editor 0.1.87 → 0.1.88

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.
@@ -1,20 +1,19 @@
1
1
  /// <reference path="./lib/fresh.d.ts" />
2
2
  const editor = getEditor();
3
3
 
4
-
5
4
  /**
6
5
  * C# Language Server Support Plugin
7
6
  *
8
- * Handles LSP server requests from C# language servers like:
9
- * - csharp-ls
10
- * - csharp-language-server (Roslyn-based)
11
- * - OmniSharp
12
- *
13
- * Features:
7
+ * Provides comprehensive C# LSP support including:
8
+ * - Project root detection (finds nearest .csproj/.sln)
14
9
  * - Auto-restore NuGet packages when opening C# files
15
- * - Auto-restore NuGet packages when the server requests it
10
+ * - Handle LSP server requests (e.g., workspace/_roslyn_projectNeedsRestore)
11
+ * - User-friendly error handling when csharp-ls fails to start
12
+ * - Installation help popup with commands to install csharp-ls
16
13
  */
17
14
 
15
+ // ==================== Type Definitions ====================
16
+
18
17
  interface LspServerRequestData {
19
18
  language: string;
20
19
  method: string;
@@ -22,6 +21,23 @@ interface LspServerRequestData {
22
21
  params: string | null;
23
22
  }
24
23
 
24
+ interface LspServerErrorData {
25
+ language: string;
26
+ server_command: string;
27
+ error_type: string;
28
+ message: string;
29
+ }
30
+
31
+ interface LspStatusClickedData {
32
+ language: string;
33
+ has_error: boolean;
34
+ }
35
+
36
+ interface ActionPopupResultData {
37
+ popup_id: string;
38
+ action_id: string;
39
+ }
40
+
25
41
  interface ProjectNeedsRestoreParams {
26
42
  projectFilePath: string;
27
43
  }
@@ -31,12 +47,25 @@ interface AfterFileOpenData {
31
47
  buffer_id: number;
32
48
  }
33
49
 
50
+ // ==================== State ====================
51
+
34
52
  // Track which directories we've already restored to avoid repeated restores
35
53
  const restoredDirectories = new Set<string>();
36
54
 
55
+ // Track which project roots we've already set for LSP
56
+ const configuredProjectRoots = new Set<string>();
57
+
37
58
  // Cache whether dotnet is available (null = not checked yet)
38
59
  let dotnetAvailable: boolean | null = null;
39
60
 
61
+ // Track error state for C# LSP
62
+ let csharpLspError: { serverCommand: string; message: string } | null = null;
63
+
64
+ // Install command for csharp-ls
65
+ const INSTALL_COMMAND = "dotnet tool install --global csharp-ls";
66
+
67
+ // ==================== Utility Functions ====================
68
+
40
69
  /**
41
70
  * Check if dotnet CLI is available
42
71
  */
@@ -59,11 +88,59 @@ async function isDotnetAvailable(): Promise<boolean> {
59
88
  return dotnetAvailable;
60
89
  }
61
90
 
91
+ /**
92
+ * Get the directory containing the file
93
+ */
94
+ function getDirectory(filePath: string): string {
95
+ return editor.pathDirname(filePath);
96
+ }
97
+
98
+ /**
99
+ * Find the project root by walking up directories looking for .csproj or .sln files
100
+ * Returns the directory containing the project file, or null if not found
101
+ */
102
+ function findProjectRoot(startPath: string): string | null {
103
+ let currentDir = getDirectory(startPath);
104
+ const maxDepth = 20; // Prevent infinite loops
105
+ let depth = 0;
106
+
107
+ while (depth < maxDepth) {
108
+ // Check if this directory contains a .csproj or .sln file
109
+ try {
110
+ const entries = editor.readDir(currentDir);
111
+ for (const entry of entries) {
112
+ if (entry.is_file) {
113
+ if (entry.name.endsWith(".csproj") || entry.name.endsWith(".sln")) {
114
+ editor.debug(`csharp_support: Found project file ${entry.name} in ${currentDir}`);
115
+ return currentDir;
116
+ }
117
+ }
118
+ }
119
+ } catch (e) {
120
+ // Directory read failed, stop searching
121
+ editor.debug(`csharp_support: Failed to read directory ${currentDir}: ${e}`);
122
+ break;
123
+ }
124
+
125
+ // Move up to parent directory
126
+ const parentDir = editor.pathDirname(currentDir);
127
+ if (parentDir === currentDir || parentDir === "/" || parentDir === "") {
128
+ // Reached root or can't go higher
129
+ break;
130
+ }
131
+ currentDir = parentDir;
132
+ depth++;
133
+ }
134
+
135
+ editor.debug(`csharp_support: No .csproj or .sln found for ${startPath}`);
136
+ return null;
137
+ }
138
+
62
139
  /**
63
140
  * Run dotnet restore for a project
64
141
  */
65
142
  async function restoreProject(projectPath: string): Promise<void> {
66
- if (!await isDotnetAvailable()) {
143
+ if (!(await isDotnetAvailable())) {
67
144
  return;
68
145
  }
69
146
 
@@ -87,20 +164,63 @@ async function restoreProject(projectPath: string): Promise<void> {
87
164
  }
88
165
  }
89
166
 
167
+ // ==================== Event Handlers ====================
168
+
169
+ /**
170
+ * Handle file open - set project root and restore packages
171
+ */
172
+ globalThis.on_csharp_file_open = async function (data: AfterFileOpenData): Promise<void> {
173
+ // Only handle .cs files
174
+ if (!data.path.endsWith(".cs")) {
175
+ return;
176
+ }
177
+
178
+ editor.debug(`csharp_support: C# file opened: ${data.path}`);
179
+
180
+ // Find the project root
181
+ const projectRoot = findProjectRoot(data.path);
182
+
183
+ if (projectRoot) {
184
+ // Set the LSP root URI if we haven't already for this project
185
+ if (!configuredProjectRoots.has(projectRoot)) {
186
+ configuredProjectRoots.add(projectRoot);
187
+
188
+ // Convert path to file:// URI
189
+ const rootUri = `file://${projectRoot}`;
190
+ editor.debug(`csharp_support: Setting LSP root URI to ${rootUri}`);
191
+ editor.setLspRootUri("csharp", rootUri);
192
+ }
193
+
194
+ // Run dotnet restore if we haven't already for this directory
195
+ if (!restoredDirectories.has(projectRoot)) {
196
+ restoredDirectories.add(projectRoot);
197
+ editor.debug(`csharp_support: Running dotnet restore in ${projectRoot}`);
198
+ await restoreProject(projectRoot);
199
+ }
200
+ } else {
201
+ // No project file found - use file's directory for restore
202
+ const dir = getDirectory(data.path);
203
+ if (!restoredDirectories.has(dir)) {
204
+ restoredDirectories.add(dir);
205
+ editor.debug(`csharp_support: No project found, running dotnet restore in ${dir}`);
206
+ await restoreProject(dir);
207
+ }
208
+ }
209
+ };
210
+
211
+ // Register hook for file open
212
+ editor.on("after_file_open", "on_csharp_file_open");
213
+
90
214
  /**
91
215
  * Handle LSP server requests from C# language servers (Roslyn-based)
92
216
  */
93
- globalThis.on_csharp_lsp_server_request = function (
94
- data: LspServerRequestData
95
- ): void {
217
+ globalThis.on_csharp_lsp_server_request = function (data: LspServerRequestData): void {
96
218
  // Only handle requests from C# language servers
97
219
  if (data.server_command !== "csharp-ls" && data.server_command !== "csharp-language-server") {
98
220
  return;
99
221
  }
100
222
 
101
- editor.debug(
102
- `csharp_support: Received LSP request ${data.method} from ${data.server_command}`
103
- );
223
+ editor.debug(`csharp_support: Received LSP request ${data.method} from ${data.server_command}`);
104
224
 
105
225
  switch (data.method) {
106
226
  case "workspace/_roslyn_projectNeedsRestore": {
@@ -120,9 +240,7 @@ globalThis.on_csharp_lsp_server_request = function (
120
240
 
121
241
  default:
122
242
  // Log unhandled requests for debugging
123
- editor.debug(
124
- `csharp_support: Unhandled LSP request: ${data.method}`
125
- );
243
+ editor.debug(`csharp_support: Unhandled LSP request: ${data.method}`);
126
244
  }
127
245
  };
128
246
 
@@ -130,41 +248,96 @@ globalThis.on_csharp_lsp_server_request = function (
130
248
  editor.on("lsp_server_request", "on_csharp_lsp_server_request");
131
249
 
132
250
  /**
133
- * Get the directory containing the file
251
+ * Handle LSP server errors for C#
134
252
  */
135
- function getDirectory(filePath: string): string {
136
- const lastSlash = filePath.lastIndexOf("/");
137
- if (lastSlash === -1) {
138
- return ".";
253
+ globalThis.on_csharp_lsp_server_error = function (data: LspServerErrorData): void {
254
+ // Only handle C# language errors
255
+ if (data.language !== "csharp") {
256
+ return;
139
257
  }
140
- return filePath.substring(0, lastSlash);
141
- }
258
+
259
+ editor.debug(`csharp_support: Server error - ${data.error_type}: ${data.message}`);
260
+
261
+ // Store error state for later reference
262
+ csharpLspError = {
263
+ serverCommand: data.server_command,
264
+ message: data.message,
265
+ };
266
+
267
+ // Show a status message for immediate feedback
268
+ if (data.error_type === "not_found") {
269
+ editor.setStatus(
270
+ `C# LSP server '${data.server_command}' not found. Click status bar for help.`
271
+ );
272
+ } else {
273
+ editor.setStatus(`C# LSP error: ${data.message}`);
274
+ }
275
+ };
276
+
277
+ // Register hook for LSP server errors
278
+ editor.on("lsp_server_error", "on_csharp_lsp_server_error");
142
279
 
143
280
  /**
144
- * Proactively run dotnet restore when opening a C# file
145
- * This ensures the LSP server has access to restored packages from the start
281
+ * Handle status bar click when there's a C# LSP error
146
282
  */
147
- globalThis.on_csharp_file_open = async function (
148
- data: AfterFileOpenData
149
- ): Promise<void> {
150
- // Only handle .cs files
151
- if (!data.path.endsWith(".cs")) {
283
+ globalThis.on_csharp_lsp_status_clicked = function (data: LspStatusClickedData): void {
284
+ // Only handle C# language clicks when there's an error
285
+ if (data.language !== "csharp" || !csharpLspError) {
152
286
  return;
153
287
  }
154
288
 
155
- const dir = getDirectory(data.path);
289
+ editor.debug("csharp_support: Status clicked, showing help popup");
156
290
 
157
- // Skip if we've already restored this directory
158
- if (restoredDirectories.has(dir)) {
291
+ // Show action popup with install options
292
+ editor.showActionPopup({
293
+ id: "csharp-lsp-help",
294
+ title: "C# Language Server Not Found",
295
+ message: `"${csharpLspError.serverCommand}" provides code completion, diagnostics, and navigation for C# files. Requires .NET SDK. Copy the command below to install it.`,
296
+ actions: [
297
+ { id: "copy_dotnet", label: `Copy: ${INSTALL_COMMAND}` },
298
+ { id: "disable", label: "Disable C# LSP" },
299
+ { id: "dismiss", label: "Dismiss (ESC)" },
300
+ ],
301
+ });
302
+ };
303
+
304
+ // Register hook for status bar clicks
305
+ editor.on("lsp_status_clicked", "on_csharp_lsp_status_clicked");
306
+
307
+ /**
308
+ * Handle action popup results for C# LSP help
309
+ */
310
+ globalThis.on_csharp_lsp_action_result = function (data: ActionPopupResultData): void {
311
+ // Only handle our popup
312
+ if (data.popup_id !== "csharp-lsp-help") {
159
313
  return;
160
314
  }
161
315
 
162
- // Mark as restored (even before we try, to avoid repeated attempts)
163
- restoredDirectories.add(dir);
316
+ editor.debug(`csharp_support: Action selected - ${data.action_id}`);
164
317
 
165
- editor.debug(`csharp_support: C# file opened, running dotnet restore in ${dir}`);
166
- await restoreProject(dir);
318
+ switch (data.action_id) {
319
+ case "copy_dotnet":
320
+ editor.setClipboard(INSTALL_COMMAND);
321
+ editor.setStatus("Copied: " + INSTALL_COMMAND);
322
+ break;
323
+
324
+ case "disable":
325
+ editor.disableLspForLanguage("csharp");
326
+ editor.setStatus("C# LSP disabled");
327
+ csharpLspError = null;
328
+ break;
329
+
330
+ case "dismiss":
331
+ case "dismissed":
332
+ // Just close the popup without action
333
+ break;
334
+
335
+ default:
336
+ editor.debug(`csharp_support: Unknown action: ${data.action_id}`);
337
+ }
167
338
  };
168
339
 
169
- // Register hook for file open
170
- editor.on("after_file_open", "on_csharp_file_open");
340
+ // Register hook for action popup results
341
+ editor.on("action_popup_result", "on_csharp_lsp_action_result");
342
+
343
+ editor.debug("csharp_support: Plugin loaded");