@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.
- package/CHANGELOG.md +144 -0
- package/package.json +1 -1
- package/plugins/astro-lsp.ts +118 -0
- package/plugins/bash-lsp.ts +161 -0
- package/plugins/clojure-lsp.ts +125 -0
- package/plugins/cmake-lsp.ts +138 -0
- package/plugins/config-schema.json +275 -29
- package/plugins/dart-lsp.ts +144 -0
- package/plugins/diagnostics_panel.ts +4 -12
- package/plugins/diff_nav.i18n.json +128 -0
- package/plugins/diff_nav.ts +196 -0
- package/plugins/elixir-lsp.ts +120 -0
- package/plugins/erlang-lsp.ts +121 -0
- package/plugins/fsharp-lsp.ts +125 -0
- package/plugins/git_gutter.ts +5 -0
- package/plugins/gleam-lsp.ts +124 -0
- package/plugins/graphql-lsp.ts +139 -0
- package/plugins/haskell-lsp.ts +125 -0
- package/plugins/julia-lsp.ts +111 -0
- package/plugins/kotlin-lsp.ts +162 -0
- package/plugins/lib/finder.ts +19 -12
- package/plugins/lib/fresh.d.ts +30 -1
- package/plugins/lua-lsp.ts +161 -0
- package/plugins/nim-lsp.ts +118 -0
- package/plugins/nix-lsp.ts +125 -0
- package/plugins/nushell-lsp.ts +144 -0
- package/plugins/ocaml-lsp.ts +119 -0
- package/plugins/perl-lsp.ts +118 -0
- package/plugins/php-lsp.ts +165 -0
- package/plugins/pkg.ts +37 -76
- package/plugins/protobuf-lsp.ts +144 -0
- package/plugins/r-lsp.ts +118 -0
- package/plugins/ruby-lsp.ts +165 -0
- package/plugins/scala-lsp.ts +119 -0
- package/plugins/schemas/package.schema.json +437 -272
- package/plugins/schemas/theme.schema.json +18 -0
- package/plugins/solidity-lsp.ts +130 -0
- package/plugins/sql-lsp.ts +129 -0
- package/plugins/svelte-lsp.ts +119 -0
- package/plugins/swift-lsp.ts +120 -0
- package/plugins/tailwindcss-lsp.ts +119 -0
- package/plugins/terraform-lsp.ts +144 -0
- package/plugins/theme_editor.i18n.json +70 -14
- package/plugins/theme_editor.ts +71 -39
- package/plugins/toml-lsp.ts +162 -0
- package/plugins/typst-lsp.ts +165 -0
- package/plugins/vue-lsp.ts +118 -0
- package/plugins/yaml-lsp.ts +163 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dart LSP Helper Plugin
|
|
6
|
+
*
|
|
7
|
+
* Provides user-friendly error handling for Dart LSP server issues.
|
|
8
|
+
* The Dart language server is included with the Dart SDK.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Detects Dart LSP server errors
|
|
12
|
+
* - Shows popup with install instructions for Dart SDK
|
|
13
|
+
* - Provides option to disable Dart LSP
|
|
14
|
+
*
|
|
15
|
+
* VS Code: "Dart" extension by Dart Code (uses bundled analysis server)
|
|
16
|
+
* Neovim: nvim-lspconfig dartls
|
|
17
|
+
* Note: The analysis server is bundled with Dart SDK and Flutter SDK
|
|
18
|
+
* For Flutter: Install Flutter SDK which includes Dart
|
|
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 Dart SDK (which includes the language server)
|
|
39
|
+
// See: https://dart.dev/get-dart
|
|
40
|
+
const INSTALL_COMMANDS = {
|
|
41
|
+
brew: "brew install dart",
|
|
42
|
+
apt: "sudo apt install dart",
|
|
43
|
+
choco: "choco install dart-sdk",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Track error state for Dart LSP
|
|
47
|
+
let dartLspError: { serverCommand: string; message: string } | null = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle LSP server errors for Dart
|
|
51
|
+
*/
|
|
52
|
+
function on_dart_lsp_server_error(data: LspServerErrorData): void {
|
|
53
|
+
if (data.language !== "dart") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
editor.debug(`dart-lsp: Server error - ${data.error_type}: ${data.message}`);
|
|
58
|
+
|
|
59
|
+
dartLspError = {
|
|
60
|
+
serverCommand: data.server_command,
|
|
61
|
+
message: data.message,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (data.error_type === "not_found") {
|
|
65
|
+
editor.setStatus(
|
|
66
|
+
`Dart LSP server '${data.server_command}' not found. Click status bar for help.`
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
editor.setStatus(`Dart LSP error: ${data.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
registerHandler("on_dart_lsp_server_error", on_dart_lsp_server_error);
|
|
73
|
+
editor.on("lsp_server_error", "on_dart_lsp_server_error");
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle status bar click when there's a Dart LSP error
|
|
77
|
+
*/
|
|
78
|
+
function on_dart_lsp_status_clicked(data: LspStatusClickedData): void {
|
|
79
|
+
if (data.language !== "dart" || !dartLspError) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
editor.debug("dart-lsp: Status clicked, showing help popup");
|
|
84
|
+
|
|
85
|
+
editor.showActionPopup({
|
|
86
|
+
id: "dart-lsp-help",
|
|
87
|
+
title: "Dart Language Server Not Found",
|
|
88
|
+
message: `The Dart language server is included with the Dart SDK. Install the Dart SDK (or Flutter SDK) to get LSP support. Visit https://dart.dev/get-dart for platform-specific instructions.`,
|
|
89
|
+
actions: [
|
|
90
|
+
{ id: "copy_brew", label: `Copy: ${INSTALL_COMMANDS.brew}` },
|
|
91
|
+
{ id: "copy_apt", label: `Copy: ${INSTALL_COMMANDS.apt}` },
|
|
92
|
+
{ id: "copy_choco", label: `Copy: ${INSTALL_COMMANDS.choco} (Windows)` },
|
|
93
|
+
{ id: "disable", label: "Disable Dart LSP" },
|
|
94
|
+
{ id: "dismiss", label: "Dismiss (ESC)" },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
registerHandler("on_dart_lsp_status_clicked", on_dart_lsp_status_clicked);
|
|
99
|
+
editor.on("lsp_status_clicked", "on_dart_lsp_status_clicked");
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle action popup results for Dart LSP help
|
|
103
|
+
*/
|
|
104
|
+
function on_dart_lsp_action_result(data: ActionPopupResultData): void {
|
|
105
|
+
if (data.popup_id !== "dart-lsp-help") {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
editor.debug(`dart-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_apt":
|
|
118
|
+
editor.setClipboard(INSTALL_COMMANDS.apt);
|
|
119
|
+
editor.setStatus("Copied: " + INSTALL_COMMANDS.apt);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case "copy_choco":
|
|
123
|
+
editor.setClipboard(INSTALL_COMMANDS.choco);
|
|
124
|
+
editor.setStatus("Copied: " + INSTALL_COMMANDS.choco);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case "disable":
|
|
128
|
+
editor.disableLspForLanguage("dart");
|
|
129
|
+
editor.setStatus("Dart LSP disabled");
|
|
130
|
+
dartLspError = null;
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case "dismiss":
|
|
134
|
+
case "dismissed":
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
editor.debug(`dart-lsp: Unknown action: ${data.action_id}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
registerHandler("on_dart_lsp_action_result", on_dart_lsp_action_result);
|
|
142
|
+
editor.on("action_popup_result", "on_dart_lsp_action_result");
|
|
143
|
+
|
|
144
|
+
editor.debug("dart-lsp: Plugin loaded");
|
|
@@ -126,6 +126,10 @@ const finder = new Finder<DiagnosticItem>(editor, {
|
|
|
126
126
|
groupBy: "file",
|
|
127
127
|
syncWithEditor: true,
|
|
128
128
|
navigateOnCursorMove: true,
|
|
129
|
+
onClose: () => {
|
|
130
|
+
isOpen = false;
|
|
131
|
+
sourceBufferId = null;
|
|
132
|
+
},
|
|
129
133
|
});
|
|
130
134
|
|
|
131
135
|
// Get title based on current filter state
|
|
@@ -244,18 +248,6 @@ registerHandler("on_diagnostics_buffer_activated", on_diagnostics_buffer_activat
|
|
|
244
248
|
editor.on("diagnostics_updated", "on_diagnostics_updated");
|
|
245
249
|
editor.on("buffer_activated", "on_diagnostics_buffer_activated");
|
|
246
250
|
|
|
247
|
-
// Mode Definition (for custom keybindings beyond Enter/Escape)
|
|
248
|
-
editor.defineMode(
|
|
249
|
-
"diagnostics-extra",
|
|
250
|
-
[
|
|
251
|
-
["a", "diagnostics_toggle_all"],
|
|
252
|
-
["r", "diagnostics_refresh"],
|
|
253
|
-
["Return", `_finder_diagnostics_panel_select`],
|
|
254
|
-
["Escape", `_finder_diagnostics_panel_close`],
|
|
255
|
-
],
|
|
256
|
-
true
|
|
257
|
-
);
|
|
258
|
-
|
|
259
251
|
// Command Registration
|
|
260
252
|
editor.registerCommand(
|
|
261
253
|
"%cmd.show_diagnostics_panel",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"en": {
|
|
3
|
+
"cmd.next_change": "Next Change",
|
|
4
|
+
"cmd.next_change_desc": "Jump to the next changed region",
|
|
5
|
+
"cmd.prev_change": "Previous Change",
|
|
6
|
+
"cmd.prev_change_desc": "Jump to the previous changed region",
|
|
7
|
+
"status.no_changes": "No changes",
|
|
8
|
+
"status.change": "Change %{n}/%{total}",
|
|
9
|
+
"status.change_wrapped": "Change %{n}/%{total} [wrapped]"
|
|
10
|
+
},
|
|
11
|
+
"cs": {
|
|
12
|
+
"cmd.next_change": "Další změna",
|
|
13
|
+
"cmd.next_change_desc": "Přejít na další změněnou oblast",
|
|
14
|
+
"cmd.prev_change": "Předchozí změna",
|
|
15
|
+
"cmd.prev_change_desc": "Přejít na předchozí změněnou oblast",
|
|
16
|
+
"status.no_changes": "Žádné změny",
|
|
17
|
+
"status.change": "Změna %{n}/%{total}",
|
|
18
|
+
"status.change_wrapped": "Změna %{n}/%{total} [zabaleno]"
|
|
19
|
+
},
|
|
20
|
+
"de": {
|
|
21
|
+
"cmd.next_change": "Nächste Änderung",
|
|
22
|
+
"cmd.next_change_desc": "Zur nächsten geänderten Region springen",
|
|
23
|
+
"cmd.prev_change": "Vorherige Änderung",
|
|
24
|
+
"cmd.prev_change_desc": "Zur vorherigen geänderten Region springen",
|
|
25
|
+
"status.no_changes": "Keine Änderungen",
|
|
26
|
+
"status.change": "Änderung %{n}/%{total}",
|
|
27
|
+
"status.change_wrapped": "Änderung %{n}/%{total} [umgebrochen]"
|
|
28
|
+
},
|
|
29
|
+
"es": {
|
|
30
|
+
"cmd.next_change": "Siguiente cambio",
|
|
31
|
+
"cmd.next_change_desc": "Saltar a la siguiente región modificada",
|
|
32
|
+
"cmd.prev_change": "Cambio anterior",
|
|
33
|
+
"cmd.prev_change_desc": "Saltar a la región modificada anterior",
|
|
34
|
+
"status.no_changes": "Sin cambios",
|
|
35
|
+
"status.change": "Cambio %{n}/%{total}",
|
|
36
|
+
"status.change_wrapped": "Cambio %{n}/%{total} [envuelto]"
|
|
37
|
+
},
|
|
38
|
+
"fr": {
|
|
39
|
+
"cmd.next_change": "Modification suivante",
|
|
40
|
+
"cmd.next_change_desc": "Aller à la prochaine région modifiée",
|
|
41
|
+
"cmd.prev_change": "Modification précédente",
|
|
42
|
+
"cmd.prev_change_desc": "Aller à la région modifiée précédente",
|
|
43
|
+
"status.no_changes": "Aucune modification",
|
|
44
|
+
"status.change": "Modification %{n}/%{total}",
|
|
45
|
+
"status.change_wrapped": "Modification %{n}/%{total} [bouclé]"
|
|
46
|
+
},
|
|
47
|
+
"it": {
|
|
48
|
+
"cmd.next_change": "Modifica successiva",
|
|
49
|
+
"cmd.next_change_desc": "Vai alla prossima regione modificata",
|
|
50
|
+
"cmd.prev_change": "Modifica precedente",
|
|
51
|
+
"cmd.prev_change_desc": "Vai alla regione modificata precedente",
|
|
52
|
+
"status.no_changes": "Nessuna modifica",
|
|
53
|
+
"status.change": "Modifica %{n}/%{total}",
|
|
54
|
+
"status.change_wrapped": "Modifica %{n}/%{total} [avvolto]"
|
|
55
|
+
},
|
|
56
|
+
"ja": {
|
|
57
|
+
"cmd.next_change": "次の変更",
|
|
58
|
+
"cmd.next_change_desc": "次の変更箇所にジャンプ",
|
|
59
|
+
"cmd.prev_change": "前の変更",
|
|
60
|
+
"cmd.prev_change_desc": "前の変更箇所にジャンプ",
|
|
61
|
+
"status.no_changes": "変更なし",
|
|
62
|
+
"status.change": "変更 %{n}/%{total}",
|
|
63
|
+
"status.change_wrapped": "変更 %{n}/%{total} [折り返し]"
|
|
64
|
+
},
|
|
65
|
+
"ko": {
|
|
66
|
+
"cmd.next_change": "다음 변경",
|
|
67
|
+
"cmd.next_change_desc": "다음 변경된 영역으로 이동",
|
|
68
|
+
"cmd.prev_change": "이전 변경",
|
|
69
|
+
"cmd.prev_change_desc": "이전 변경된 영역으로 이동",
|
|
70
|
+
"status.no_changes": "변경 없음",
|
|
71
|
+
"status.change": "변경 %{n}/%{total}",
|
|
72
|
+
"status.change_wrapped": "변경 %{n}/%{total} [순환]"
|
|
73
|
+
},
|
|
74
|
+
"pt-BR": {
|
|
75
|
+
"cmd.next_change": "Próxima alteração",
|
|
76
|
+
"cmd.next_change_desc": "Ir para a próxima região alterada",
|
|
77
|
+
"cmd.prev_change": "Alteração anterior",
|
|
78
|
+
"cmd.prev_change_desc": "Ir para a região alterada anterior",
|
|
79
|
+
"status.no_changes": "Sem alterações",
|
|
80
|
+
"status.change": "Alteração %{n}/%{total}",
|
|
81
|
+
"status.change_wrapped": "Alteração %{n}/%{total} [retornou]"
|
|
82
|
+
},
|
|
83
|
+
"ru": {
|
|
84
|
+
"cmd.next_change": "Следующее изменение",
|
|
85
|
+
"cmd.next_change_desc": "Перейти к следующей изменённой области",
|
|
86
|
+
"cmd.prev_change": "Предыдущее изменение",
|
|
87
|
+
"cmd.prev_change_desc": "Перейти к предыдущей изменённой области",
|
|
88
|
+
"status.no_changes": "Нет изменений",
|
|
89
|
+
"status.change": "Изменение %{n}/%{total}",
|
|
90
|
+
"status.change_wrapped": "Изменение %{n}/%{total} [цикл]"
|
|
91
|
+
},
|
|
92
|
+
"th": {
|
|
93
|
+
"cmd.next_change": "การเปลี่ยนแปลงถัดไป",
|
|
94
|
+
"cmd.next_change_desc": "ข้ามไปยังพื้นที่ที่เปลี่ยนแปลงถัดไป",
|
|
95
|
+
"cmd.prev_change": "การเปลี่ยนแปลงก่อนหน้า",
|
|
96
|
+
"cmd.prev_change_desc": "ข้ามไปยังพื้นที่ที่เปลี่ยนแปลงก่อนหน้า",
|
|
97
|
+
"status.no_changes": "ไม่มีการเปลี่ยนแปลง",
|
|
98
|
+
"status.change": "การเปลี่ยนแปลง %{n}/%{total}",
|
|
99
|
+
"status.change_wrapped": "การเปลี่ยนแปลง %{n}/%{total} [วนรอบ]"
|
|
100
|
+
},
|
|
101
|
+
"uk": {
|
|
102
|
+
"cmd.next_change": "Наступна зміна",
|
|
103
|
+
"cmd.next_change_desc": "Перейти до наступної зміненої області",
|
|
104
|
+
"cmd.prev_change": "Попередня зміна",
|
|
105
|
+
"cmd.prev_change_desc": "Перейти до попередньої зміненої області",
|
|
106
|
+
"status.no_changes": "Немає змін",
|
|
107
|
+
"status.change": "Зміна %{n}/%{total}",
|
|
108
|
+
"status.change_wrapped": "Зміна %{n}/%{total} [цикл]"
|
|
109
|
+
},
|
|
110
|
+
"vi": {
|
|
111
|
+
"cmd.next_change": "Thay đổi tiếp theo",
|
|
112
|
+
"cmd.next_change_desc": "Nhảy đến vùng thay đổi tiếp theo",
|
|
113
|
+
"cmd.prev_change": "Thay đổi trước đó",
|
|
114
|
+
"cmd.prev_change_desc": "Nhảy đến vùng thay đổi trước đó",
|
|
115
|
+
"status.no_changes": "Không có thay đổi",
|
|
116
|
+
"status.change": "Thay đổi %{n}/%{total}",
|
|
117
|
+
"status.change_wrapped": "Thay đổi %{n}/%{total} [quay vòng]"
|
|
118
|
+
},
|
|
119
|
+
"zh-CN": {
|
|
120
|
+
"cmd.next_change": "下一个更改",
|
|
121
|
+
"cmd.next_change_desc": "跳转到下一个已更改的区域",
|
|
122
|
+
"cmd.prev_change": "上一个更改",
|
|
123
|
+
"cmd.prev_change_desc": "跳转到上一个已更改的区域",
|
|
124
|
+
"status.no_changes": "没有更改",
|
|
125
|
+
"status.change": "更改 %{n}/%{total}",
|
|
126
|
+
"status.change_wrapped": "更改 %{n}/%{total} [已循环]"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Diff Navigation Plugin
|
|
6
|
+
*
|
|
7
|
+
* Provides unified next/previous change commands that merge changes from all
|
|
8
|
+
* available diff sources: git diff AND piece-tree saved-diff. This means a
|
|
9
|
+
* single keybinding pair navigates both committed and unsaved changes.
|
|
10
|
+
*
|
|
11
|
+
* When only one source is available (e.g. file not tracked by git), it still
|
|
12
|
+
* works using that source alone.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
interface DiffHunk {
|
|
20
|
+
type: "added" | "modified" | "deleted";
|
|
21
|
+
startLine: number; // 1-indexed
|
|
22
|
+
lineCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A jump target with a byte position for sorting/deduplication */
|
|
26
|
+
interface JumpTarget {
|
|
27
|
+
bytePos: number;
|
|
28
|
+
line: number; // 0-indexed, for scrollToLineCenter
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Collecting jump targets from all sources
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
async function collectTargets(bid: number): Promise<JumpTarget[]> {
|
|
36
|
+
const targets: JumpTarget[] = [];
|
|
37
|
+
|
|
38
|
+
// Source 1: git gutter hunks
|
|
39
|
+
const hunks = editor.getViewState(bid, "git_gutter_hunks") as DiffHunk[] | null;
|
|
40
|
+
if (hunks && hunks.length > 0) {
|
|
41
|
+
for (const hunk of hunks) {
|
|
42
|
+
const line = Math.max(0, hunk.startLine - 1); // 0-indexed
|
|
43
|
+
const pos = await editor.getLineStartPosition(line);
|
|
44
|
+
if (pos !== null) {
|
|
45
|
+
targets.push({ bytePos: pos, line });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Source 2: saved-diff (unsaved changes)
|
|
51
|
+
const diff = editor.getBufferSavedDiff(bid);
|
|
52
|
+
if (diff && !diff.equal) {
|
|
53
|
+
for (const [start, _end] of diff.byte_ranges) {
|
|
54
|
+
// We don't know the line yet; resolve it lazily after dedup
|
|
55
|
+
targets.push({ bytePos: start, line: -1 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (targets.length === 0) return targets;
|
|
60
|
+
|
|
61
|
+
// Sort by byte position
|
|
62
|
+
targets.sort((a, b) => a.bytePos - b.bytePos);
|
|
63
|
+
|
|
64
|
+
// Deduplicate: if two targets are on the same line, keep the first.
|
|
65
|
+
// Resolve line numbers for saved-diff targets that still have line = -1.
|
|
66
|
+
const deduped: JumpTarget[] = [];
|
|
67
|
+
const seenLines = new Set<number>();
|
|
68
|
+
|
|
69
|
+
for (const t of targets) {
|
|
70
|
+
// Resolve line if unknown
|
|
71
|
+
if (t.line === -1) {
|
|
72
|
+
// Jump cursor temporarily to find the line, then restore.
|
|
73
|
+
// Instead, use a simpler heuristic: find the line by checking
|
|
74
|
+
// existing targets or using getLineStartPosition in reverse.
|
|
75
|
+
// Actually, we can set cursor, read line, but that's side-effectful.
|
|
76
|
+
// Simpler: just check if any existing target has a bytePos close enough.
|
|
77
|
+
// For dedup, we check if any already-added target has same bytePos.
|
|
78
|
+
let isDup = false;
|
|
79
|
+
for (const existing of deduped) {
|
|
80
|
+
if (Math.abs(existing.bytePos - t.bytePos) < 2) {
|
|
81
|
+
isDup = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (isDup) continue;
|
|
86
|
+
deduped.push(t);
|
|
87
|
+
} else {
|
|
88
|
+
if (seenLines.has(t.line)) continue;
|
|
89
|
+
seenLines.add(t.line);
|
|
90
|
+
// Also check if a saved-diff target at similar byte pos was already added
|
|
91
|
+
let isDup = false;
|
|
92
|
+
for (const existing of deduped) {
|
|
93
|
+
if (existing.line === -1 && Math.abs(existing.bytePos - t.bytePos) < 2) {
|
|
94
|
+
// Replace the unresolved one with this one (which has a known line)
|
|
95
|
+
existing.line = t.line;
|
|
96
|
+
isDup = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (isDup) continue;
|
|
101
|
+
deduped.push(t);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return deduped;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// Navigation
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
function goToTarget(bid: number, target: JumpTarget): void {
|
|
113
|
+
if (target.line >= 0) {
|
|
114
|
+
const splitId = editor.getActiveSplitId();
|
|
115
|
+
editor.scrollToLineCenter(splitId, bid, target.line);
|
|
116
|
+
}
|
|
117
|
+
editor.setBufferCursor(bid, target.bytePos);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function diff_nav_next(): Promise<void> {
|
|
121
|
+
const bid = editor.getActiveBufferId();
|
|
122
|
+
const targets = await collectTargets(bid);
|
|
123
|
+
|
|
124
|
+
if (targets.length === 0) {
|
|
125
|
+
editor.setStatus(editor.t("status.no_changes"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cursor = editor.getCursorPosition();
|
|
130
|
+
let idx = targets.findIndex((t) => t.bytePos > cursor);
|
|
131
|
+
let wrapped = false;
|
|
132
|
+
if (idx === -1) {
|
|
133
|
+
idx = 0;
|
|
134
|
+
wrapped = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
goToTarget(bid, targets[idx]);
|
|
138
|
+
|
|
139
|
+
const msg = wrapped
|
|
140
|
+
? editor.t("status.change_wrapped", { n: String(idx + 1), total: String(targets.length) })
|
|
141
|
+
: editor.t("status.change", { n: String(idx + 1), total: String(targets.length) });
|
|
142
|
+
editor.setStatus(msg);
|
|
143
|
+
}
|
|
144
|
+
registerHandler("diff_nav_next", diff_nav_next);
|
|
145
|
+
|
|
146
|
+
async function diff_nav_prev(): Promise<void> {
|
|
147
|
+
const bid = editor.getActiveBufferId();
|
|
148
|
+
const targets = await collectTargets(bid);
|
|
149
|
+
|
|
150
|
+
if (targets.length === 0) {
|
|
151
|
+
editor.setStatus(editor.t("status.no_changes"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const cursor = editor.getCursorPosition();
|
|
156
|
+
let idx = -1;
|
|
157
|
+
for (let i = targets.length - 1; i >= 0; i--) {
|
|
158
|
+
if (targets[i].bytePos < cursor) {
|
|
159
|
+
idx = i;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let wrapped = false;
|
|
164
|
+
if (idx === -1) {
|
|
165
|
+
idx = targets.length - 1;
|
|
166
|
+
wrapped = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
goToTarget(bid, targets[idx]);
|
|
170
|
+
|
|
171
|
+
const msg = wrapped
|
|
172
|
+
? editor.t("status.change_wrapped", { n: String(idx + 1), total: String(targets.length) })
|
|
173
|
+
: editor.t("status.change", { n: String(idx + 1), total: String(targets.length) });
|
|
174
|
+
editor.setStatus(msg);
|
|
175
|
+
}
|
|
176
|
+
registerHandler("diff_nav_prev", diff_nav_prev);
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Registration
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
editor.registerCommand(
|
|
183
|
+
"%cmd.next_change",
|
|
184
|
+
"%cmd.next_change_desc",
|
|
185
|
+
"diff_nav_next",
|
|
186
|
+
null
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
editor.registerCommand(
|
|
190
|
+
"%cmd.prev_change",
|
|
191
|
+
"%cmd.prev_change_desc",
|
|
192
|
+
"diff_nav_prev",
|
|
193
|
+
null
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
editor.debug("Diff Nav plugin loaded");
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Elixir LSP Helper Plugin
|
|
6
|
+
*
|
|
7
|
+
* Server: elixir-ls (github.com/elixir-lsp/elixir-ls)
|
|
8
|
+
* VS Code: "ElixirLS" extension
|
|
9
|
+
* Neovim: nvim-lspconfig elixirls
|
|
10
|
+
* Requires: Elixir and Erlang/OTP installed
|
|
11
|
+
* Note: The Elixir ecosystem is consolidating around Expert
|
|
12
|
+
* (github.com/elixir-lang/expert) as the new official LSP server
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface LspServerErrorData {
|
|
16
|
+
language: string;
|
|
17
|
+
server_command: string;
|
|
18
|
+
error_type: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LspStatusClickedData {
|
|
23
|
+
language: string;
|
|
24
|
+
has_error: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ActionPopupResultData {
|
|
28
|
+
popup_id: string;
|
|
29
|
+
action_id: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const INSTALL_COMMANDS = {
|
|
33
|
+
github: "# Download from https://github.com/elixir-lsp/elixir-ls/releases",
|
|
34
|
+
brew: "brew install elixir-ls",
|
|
35
|
+
nix: "nix-env -iA nixpkgs.elixir-ls",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let elixirLspError: { serverCommand: string; message: string } | null = null;
|
|
39
|
+
|
|
40
|
+
function on_elixir_lsp_server_error(data: LspServerErrorData): void {
|
|
41
|
+
if (data.language !== "elixir") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
editor.debug(`elixir-lsp: Server error - ${data.error_type}: ${data.message}`);
|
|
46
|
+
|
|
47
|
+
elixirLspError = {
|
|
48
|
+
serverCommand: data.server_command,
|
|
49
|
+
message: data.message,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (data.error_type === "not_found") {
|
|
53
|
+
editor.setStatus(
|
|
54
|
+
`Elixir LSP server '${data.server_command}' not found. Click status bar for help.`
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
editor.setStatus(`Elixir LSP error: ${data.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
registerHandler("on_elixir_lsp_server_error", on_elixir_lsp_server_error);
|
|
61
|
+
editor.on("lsp_server_error", "on_elixir_lsp_server_error");
|
|
62
|
+
|
|
63
|
+
function on_elixir_lsp_status_clicked(data: LspStatusClickedData): void {
|
|
64
|
+
if (data.language !== "elixir" || !elixirLspError) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
editor.debug("elixir-lsp: Status clicked, showing help popup");
|
|
69
|
+
|
|
70
|
+
editor.showActionPopup({
|
|
71
|
+
id: "elixir-lsp-help",
|
|
72
|
+
title: "Elixir Language Server Not Found",
|
|
73
|
+
message: `"${elixirLspError.serverCommand}" provides completion, diagnostics, go-to-definition, Dialyzer integration, and debugging for Elixir. Requires Elixir and Erlang/OTP.\n\nNew: Expert (https://expert-lsp.org) is the upcoming official Elixir LSP, merging ElixirLS, Lexical, and Next LS.\nVS Code users: Install the "ElixirLS" extension.\nSee: https://github.com/elixir-lsp/elixir-ls`,
|
|
74
|
+
actions: [
|
|
75
|
+
{ id: "copy_brew", label: `Copy: ${INSTALL_COMMANDS.brew}` },
|
|
76
|
+
{ id: "copy_nix", label: `Copy: ${INSTALL_COMMANDS.nix}` },
|
|
77
|
+
{ id: "disable", label: "Disable Elixir LSP" },
|
|
78
|
+
{ id: "dismiss", label: "Dismiss (ESC)" },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
registerHandler("on_elixir_lsp_status_clicked", on_elixir_lsp_status_clicked);
|
|
83
|
+
editor.on("lsp_status_clicked", "on_elixir_lsp_status_clicked");
|
|
84
|
+
|
|
85
|
+
function on_elixir_lsp_action_result(data: ActionPopupResultData): void {
|
|
86
|
+
if (data.popup_id !== "elixir-lsp-help") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
editor.debug(`elixir-lsp: Action selected - ${data.action_id}`);
|
|
91
|
+
|
|
92
|
+
switch (data.action_id) {
|
|
93
|
+
case "copy_brew":
|
|
94
|
+
editor.setClipboard(INSTALL_COMMANDS.brew);
|
|
95
|
+
editor.setStatus("Copied: " + INSTALL_COMMANDS.brew);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "copy_nix":
|
|
99
|
+
editor.setClipboard(INSTALL_COMMANDS.nix);
|
|
100
|
+
editor.setStatus("Copied: " + INSTALL_COMMANDS.nix);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "disable":
|
|
104
|
+
editor.disableLspForLanguage("elixir");
|
|
105
|
+
editor.setStatus("Elixir LSP disabled");
|
|
106
|
+
elixirLspError = null;
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case "dismiss":
|
|
110
|
+
case "dismissed":
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
default:
|
|
114
|
+
editor.debug(`elixir-lsp: Unknown action: ${data.action_id}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
registerHandler("on_elixir_lsp_action_result", on_elixir_lsp_action_result);
|
|
118
|
+
editor.on("action_popup_result", "on_elixir_lsp_action_result");
|
|
119
|
+
|
|
120
|
+
editor.debug("elixir-lsp: Plugin loaded");
|