@fresh-editor/fresh-editor 0.1.75 → 0.1.76

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 (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -0
  3. package/package.json +1 -1
  4. package/plugins/audit_mode.ts +9 -4
  5. package/plugins/buffer_modified.ts +1 -1
  6. package/plugins/calculator.ts +1 -1
  7. package/plugins/check-types.sh +41 -0
  8. package/plugins/clangd_support.ts +1 -1
  9. package/plugins/color_highlighter.ts +4 -1
  10. package/plugins/config-schema.json +44 -2
  11. package/plugins/diagnostics_panel.i18n.json +52 -52
  12. package/plugins/diagnostics_panel.ts +168 -540
  13. package/plugins/find_references.ts +82 -324
  14. package/plugins/git_blame.i18n.json +260 -247
  15. package/plugins/git_blame.ts +4 -9
  16. package/plugins/git_find_file.ts +42 -270
  17. package/plugins/git_grep.ts +50 -167
  18. package/plugins/git_gutter.ts +1 -1
  19. package/plugins/git_log.ts +4 -11
  20. package/plugins/lib/finder.ts +1499 -0
  21. package/plugins/lib/fresh.d.ts +93 -17
  22. package/plugins/lib/index.ts +14 -0
  23. package/plugins/lib/navigation-controller.ts +1 -1
  24. package/plugins/lib/panel-manager.ts +7 -13
  25. package/plugins/lib/results-panel.ts +914 -0
  26. package/plugins/lib/search-utils.ts +343 -0
  27. package/plugins/lib/virtual-buffer-factory.ts +3 -2
  28. package/plugins/live_grep.ts +56 -379
  29. package/plugins/markdown_compose.ts +1 -17
  30. package/plugins/merge_conflict.ts +16 -14
  31. package/plugins/path_complete.ts +1 -1
  32. package/plugins/search_replace.i18n.json +13 -13
  33. package/plugins/search_replace.ts +11 -9
  34. package/plugins/theme_editor.ts +15 -9
  35. package/plugins/todo_highlighter.ts +1 -0
  36. package/plugins/vi_mode.ts +9 -5
  37. package/plugins/welcome.ts +1 -1
@@ -1,4 +1,4 @@
1
- /// <reference path="../types/fresh.d.ts" />
1
+ /// <reference path="./lib/fresh.d.ts" />
2
2
  const editor = getEditor();
3
3
 
4
4
 
@@ -664,14 +664,9 @@ globalThis.git_blame_copy_hash = function(): void {
664
664
  return;
665
665
  }
666
666
 
667
- // Use spawn to copy to clipboard
668
- editor.spawnProcess("sh", ["-c", `echo -n "${hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${hash}" | pbcopy 2>/dev/null || echo -n "${hash}" | xsel --clipboard 2>/dev/null`])
669
- .then(() => {
670
- editor.setStatus(editor.t("status.hash_copied", { short: hash.slice(0, 7), full: hash }));
671
- })
672
- .catch(() => {
673
- editor.setStatus(editor.t("status.hash_display", { hash }));
674
- });
667
+ // Copy hash to clipboard
668
+ editor.copyToClipboard(hash);
669
+ editor.setStatus(editor.t("status.hash_copied", { short: hash.slice(0, 7), full: hash }));
675
670
  };
676
671
 
677
672
  // =============================================================================
@@ -1,287 +1,65 @@
1
- /// <reference path="../types/fresh.d.ts" />
2
- const editor = getEditor();
3
-
1
+ /// <reference path="./lib/fresh.d.ts" />
4
2
 
5
3
  /**
6
4
  * Git Find File Plugin
7
5
  *
8
6
  * Provides interactive file finding functionality with fuzzy search
9
- * for git-tracked files. Uses the prompt API for interactive selection.
7
+ * for git-tracked files. Uses the Finder abstraction with filter mode.
10
8
  */
11
9
 
12
- // State management
13
- let allFiles: string[] = [];
14
- let filteredFiles: string[] = [];
15
- let isLoading = false;
16
-
17
- // Simple fuzzy filter function
18
- function fuzzyMatch(str: string, pattern: string): boolean {
19
- if (pattern === "") {
20
- return true;
21
- }
22
-
23
- str = str.toLowerCase();
24
- pattern = pattern.toLowerCase();
25
-
26
- let strIdx = 0;
27
- let patIdx = 0;
28
-
29
- while (strIdx < str.length && patIdx < pattern.length) {
30
- if (str[strIdx] === pattern[patIdx]) {
31
- patIdx++;
32
- }
33
- strIdx++;
34
- }
35
-
36
- return patIdx >= pattern.length;
37
- }
38
-
39
- // Score a fuzzy match (higher is better)
40
- function fuzzyScore(str: string, pattern: string): number {
41
- if (pattern === "") return 0;
42
-
43
- str = str.toLowerCase();
44
- pattern = pattern.toLowerCase();
45
-
46
- let score = 0;
47
- let strIdx = 0;
48
- let patIdx = 0;
49
- let consecutiveMatches = 0;
50
- let lastMatchIdx = -1;
51
-
52
- while (strIdx < str.length && patIdx < pattern.length) {
53
- if (str[strIdx] === pattern[patIdx]) {
54
- // Bonus for consecutive matches
55
- if (lastMatchIdx === strIdx - 1) {
56
- consecutiveMatches++;
57
- score += consecutiveMatches * 10;
58
- } else {
59
- consecutiveMatches = 1;
60
- score += 1;
61
- }
62
-
63
- // Bonus for matching at start of path segments
64
- if (strIdx === 0 || str[strIdx - 1] === "/" || str[strIdx - 1] === "_" || str[strIdx - 1] === "-") {
65
- score += 15;
66
- }
10
+ import { Finder } from "./lib/finder.ts";
67
11
 
68
- // Bonus for matching filename (after last /)
69
- const lastSlash = str.lastIndexOf("/");
70
- if (strIdx > lastSlash) {
71
- score += 5;
72
- }
73
-
74
- lastMatchIdx = strIdx;
75
- patIdx++;
76
- }
77
- strIdx++;
78
- }
79
-
80
- // Penalty for longer paths
81
- score -= str.length * 0.1;
82
-
83
- return patIdx >= pattern.length ? score : -1;
84
- }
85
-
86
- // Filter and sort files by query using fuzzy matching
87
- function filterFiles(files: string[], query: string): string[] {
88
- if (query === "" || query.trim() === "") {
89
- // Return first 100 files for empty query
90
- return files.slice(0, 100);
91
- }
92
-
93
- const scored: Array<{ file: string; score: number }> = [];
94
-
95
- for (const file of files) {
96
- const score = fuzzyScore(file, query);
97
- if (score > 0) {
98
- scored.push({ file, score });
99
- }
100
-
101
- // Stop early if we have enough high-quality matches
102
- if (scored.length >= 500) {
103
- break;
104
- }
105
- }
106
-
107
- // Sort by score descending
108
- scored.sort((a, b) => b.score - a.score);
109
-
110
- // Return top 100 results
111
- return scored.slice(0, 100).map((s) => s.file);
112
- }
113
-
114
- // Load git-tracked files asynchronously
115
- async function loadGitFiles(): Promise<void> {
116
- if (isLoading) {
117
- return;
118
- }
119
-
120
- isLoading = true;
121
- editor.setStatus(editor.t("status.loading"));
122
-
123
- try {
124
- const result = await editor.spawnProcess("git", ["ls-files"]);
125
-
126
- if (result.exit_code === 0) {
127
- allFiles = result.stdout.split("\n").filter((line) => line.trim() !== "");
12
+ const editor = getEditor();
128
13
 
129
- editor.debug(`Loaded ${allFiles.length} git-tracked files`);
130
- editor.setStatus(editor.t("status.indexed", { count: String(allFiles.length) }));
131
- } else {
132
- editor.debug(`Failed to load git files: ${result.stderr}`);
133
- editor.setStatus(editor.t("status.error_loading", { error: result.stderr }));
134
- allFiles = [];
135
- }
136
- } catch (e) {
137
- editor.debug(`Exception loading git files: ${e}`);
138
- editor.setStatus(editor.t("status.failed_load"));
139
- allFiles = [];
140
- } finally {
141
- isLoading = false;
14
+ // Create the finder instance with filter mode
15
+ const finder = new Finder<string>(editor, {
16
+ id: "git-find-file",
17
+ format: (file) => ({
18
+ label: file,
19
+ location: { file, line: 1, column: 1 },
20
+ }),
21
+ preview: false, // No preview for file finder
22
+ maxResults: 100,
23
+ });
24
+
25
+ // Load git-tracked files
26
+ async function loadGitFiles(): Promise<string[]> {
27
+ const result = await editor.spawnProcess("git", ["ls-files"]);
28
+
29
+ if (result.exit_code === 0) {
30
+ return result.stdout.split("\n").filter((line) => line.trim() !== "");
142
31
  }
143
- }
144
32
 
145
- // Convert filtered files to prompt suggestions
146
- function filesToSuggestions(files: string[]): PromptSuggestion[] {
147
- return files.map((file) => {
148
- return {
149
- text: file,
150
- description: undefined,
151
- value: file,
152
- disabled: false,
153
- };
154
- });
33
+ editor.debug(`Failed to load git files: ${result.stderr}`);
34
+ return [];
155
35
  }
156
36
 
157
37
  // Global function to start file finder
158
- globalThis.start_git_find_file = async function (): Promise<void> {
159
- // Load files if not already loaded
160
- if (allFiles.length === 0 && !isLoading) {
161
- await loadGitFiles();
162
- }
163
-
164
- if (allFiles.length === 0) {
165
- editor.setStatus(editor.t("status.no_files"));
166
- return;
167
- }
168
-
169
- // Clear previous results
170
- filteredFiles = [];
171
-
172
- // Start the prompt
173
- editor.startPrompt(editor.t("prompt.find_file"), "git-find-file");
174
-
175
- // Show initial suggestions (first 100 files)
176
- const initial = filterFiles(allFiles, "");
177
- filteredFiles = initial;
178
- editor.setPromptSuggestions(filesToSuggestions(initial));
179
- editor.setStatus(editor.t("status.files_available", { count: String(allFiles.length) }));
180
- };
181
-
182
- // React to prompt input changes
183
- globalThis.onGitFindFilePromptChanged = function (args: { prompt_type: string; input: string }): boolean {
184
- if (args.prompt_type !== "git-find-file") {
185
- return true; // Not our prompt
186
- }
187
-
188
- const query = args.input;
189
-
190
- // Filter files based on query
191
- const matches = filterFiles(allFiles, query);
192
- filteredFiles = matches;
193
-
194
- // Update suggestions
195
- editor.setPromptSuggestions(filesToSuggestions(matches));
196
-
197
- // Update status
198
- if (matches.length > 0) {
199
- if (query.trim() === "") {
200
- editor.setStatus(editor.t("status.showing_first", { shown: String(matches.length), total: String(allFiles.length) }));
201
- } else {
202
- editor.setStatus(editor.t("status.found_matching", { count: String(matches.length), query }));
203
- }
204
- } else {
205
- editor.setStatus(editor.t("status.no_matching", { query }));
206
- }
207
-
208
- return true;
209
- };
210
-
211
- // Handle prompt confirmation (user pressed Enter)
212
- globalThis.onGitFindFilePromptConfirmed = function (args: {
213
- prompt_type: string;
214
- selected_index: number | null;
215
- input: string;
216
- }): boolean {
217
- if (args.prompt_type !== "git-find-file") {
218
- return true; // Not our prompt
219
- }
220
-
221
- editor.debug(`git-find-file confirmed: selected_index=${args.selected_index}, input=${args.input}`);
222
-
223
- // Check if user selected a suggestion
224
- if (args.selected_index !== null && filteredFiles[args.selected_index]) {
225
- const selectedFile = filteredFiles[args.selected_index];
226
-
227
- editor.debug(`Opening file: ${selectedFile}`);
228
-
229
- // Open the file at line 1
230
- editor.openFile(selectedFile, 1, 1);
231
- editor.setStatus(editor.t("status.opened", { file: selectedFile }));
232
- } else if (args.input.trim() !== "") {
233
- // Try to open input directly if it's a valid file path
234
- const inputFile = args.input.trim();
235
-
236
- // Check if the exact input matches any file
237
- if (allFiles.includes(inputFile)) {
238
- editor.openFile(inputFile, 1, 1);
239
- editor.setStatus(editor.t("status.opened", { file: inputFile }));
240
- } else {
241
- editor.setStatus(editor.t("status.file_not_found", { file: inputFile }));
242
- }
243
- } else {
244
- editor.setStatus(editor.t("status.no_selection"));
245
- }
246
-
247
- return true;
248
- };
249
-
250
- // Handle prompt cancellation (user pressed Escape)
251
- globalThis.onGitFindFilePromptCancelled = function (args: { prompt_type: string }): boolean {
252
- if (args.prompt_type !== "git-find-file") {
253
- return true; // Not our prompt
254
- }
255
-
256
- // Clear results
257
- filteredFiles = [];
258
- editor.setStatus(editor.t("status.cancelled"));
259
-
260
- return true;
38
+ globalThis.start_git_find_file = function (): void {
39
+ finder.prompt({
40
+ title: editor.t("prompt.find_file"),
41
+ source: {
42
+ mode: "filter",
43
+ load: loadGitFiles,
44
+ // Uses built-in fuzzy filter by default
45
+ },
46
+ });
261
47
  };
262
48
 
263
- // Register event handlers
264
- editor.on("prompt_changed", "onGitFindFilePromptChanged");
265
- editor.on("prompt_confirmed", "onGitFindFilePromptConfirmed");
266
- editor.on("prompt_cancelled", "onGitFindFilePromptCancelled");
267
-
268
49
  // Reload git files command
269
50
  globalThis.git_reload_files = async function (): Promise<void> {
270
- allFiles = [];
271
- await loadGitFiles();
272
- };
273
-
274
- // Show file count command
275
- globalThis.git_file_count = function (): void {
276
- if (allFiles.length === 0) {
277
- editor.setStatus(editor.t("status.no_files_loaded"));
278
- } else {
279
- editor.setStatus(editor.t("status.indexed", { count: String(allFiles.length) }));
280
- }
51
+ // Just re-trigger the prompt which will reload
52
+ globalThis.start_git_find_file();
53
+ editor.setStatus(editor.t("status.reloading"));
281
54
  };
282
55
 
283
56
  // Register commands
284
- editor.registerCommand("%cmd.find", "%cmd.find_desc", "start_git_find_file", "normal");
57
+ editor.registerCommand(
58
+ "%cmd.find",
59
+ "%cmd.find_desc",
60
+ "start_git_find_file",
61
+ "normal"
62
+ );
285
63
 
286
64
  editor.registerCommand(
287
65
  "%cmd.reload",
@@ -290,11 +68,5 @@ editor.registerCommand(
290
68
  "normal"
291
69
  );
292
70
 
293
- editor.registerCommand("%cmd.count", "%cmd.count_desc", "git_file_count", "normal");
294
-
295
- // Note: We don't load git files on plugin init because spawning processes requires async context
296
- // Files will be loaded lazily on first use
297
-
298
- editor.debug("Git Find File plugin loaded successfully (TypeScript)");
299
- editor.debug("Usage: Call start_git_find_file() or use command palette 'Git Find File'");
71
+ editor.debug("Git Find File plugin loaded (using Finder abstraction)");
300
72
  editor.setStatus(editor.t("status.ready"));
@@ -1,13 +1,17 @@
1
- /// <reference path="../types/fresh.d.ts" />
2
- const editor = getEditor();
3
-
1
+ /// <reference path="./lib/fresh.d.ts" />
4
2
 
5
3
  /**
6
4
  * Git Grep Plugin
7
5
  *
8
- * Provides interactive git grep functionality with live search results.
6
+ * Provides interactive git grep functionality with live search results
7
+ * and preview panel. Uses the Finder abstraction for unified search UX.
9
8
  */
10
9
 
10
+ import { Finder, parseGrepOutput } from "./lib/finder.ts";
11
+
12
+ const editor = getEditor();
13
+
14
+ // Result type from git grep
11
15
  interface GrepMatch {
12
16
  file: string;
13
17
  line: number;
@@ -15,177 +19,56 @@ interface GrepMatch {
15
19
  content: string;
16
20
  }
17
21
 
18
- // State management
19
- let gitGrepResults: GrepMatch[] = [];
20
-
21
- // Parse git grep output line
22
- // Format: file:line:column:content
23
- function parseGitGrepLine(line: string): GrepMatch | null {
24
- const match = line.match(/^([^:]+):(\d+):(\d+):(.*)$/);
25
- if (match) {
26
- return {
27
- file: match[1],
28
- line: parseInt(match[2], 10),
29
- column: parseInt(match[3], 10),
30
- content: match[4].trimStart(),
31
- };
32
- }
33
- return null;
34
- }
35
-
36
- // Parse git grep output into suggestions
37
- function parseGitGrepOutput(stdout: string): {
38
- results: GrepMatch[];
39
- suggestions: PromptSuggestion[];
40
- } {
41
- const results: GrepMatch[] = [];
42
- const suggestions: PromptSuggestion[] = [];
43
-
44
- for (const line of stdout.split("\n")) {
45
- if (!line.trim()) continue;
46
- const match = parseGitGrepLine(line);
47
- if (match) {
48
- results.push(match);
49
- suggestions.push({
50
- text: `${match.file}:${match.line}:${match.column}`,
51
- description: match.content,
52
- value: `${match.file}:${match.line}:${match.column}`,
53
- disabled: false,
54
- });
55
-
56
- // Limit to 100 results for performance
57
- if (results.length >= 100) {
58
- break;
59
- }
60
- }
61
- }
62
-
63
- return { results, suggestions };
64
- }
65
-
66
- // Global function to start git grep
67
- globalThis.start_git_grep = function(): void {
68
- // Clear previous results
69
- gitGrepResults = [];
70
-
71
- // Start the prompt
72
- editor.startPrompt(editor.t("prompt.grep"), "git-grep");
73
- editor.setStatus(editor.t("status.type_to_search"));
74
- };
75
-
76
- // React to prompt input changes
77
- globalThis.onGitGrepPromptChanged = function(args: {
78
- prompt_type: string;
79
- input: string;
80
- }): boolean {
81
- if (args.prompt_type !== "git-grep") {
82
- return true; // Not our prompt
83
- }
84
-
85
- const query = args.input;
86
-
87
- // Don't search for empty queries
88
- if (!query || query.trim() === "") {
89
- editor.setPromptSuggestions([]);
90
- return true;
91
- }
92
-
93
- // Spawn git grep asynchronously
22
+ // Create the finder instance
23
+ const finder = new Finder<GrepMatch>(editor, {
24
+ id: "git-grep",
25
+ format: (match) => ({
26
+ label: `${match.file}:${match.line}`,
27
+ description:
28
+ match.content.length > 60
29
+ ? match.content.substring(0, 57).trim() + "..."
30
+ : match.content.trim(),
31
+ location: {
32
+ file: match.file,
33
+ line: match.line,
34
+ column: match.column,
35
+ },
36
+ }),
37
+ preview: true,
38
+ maxResults: 100,
39
+ });
40
+
41
+ // Search function using git grep
42
+ async function searchWithGitGrep(query: string): Promise<GrepMatch[]> {
94
43
  const cwd = editor.getCwd();
95
- editor.spawnProcess("git", ["grep", "-n", "--column", "-I", "--", query], cwd)
96
- .then((result) => {
97
- if (result.exit_code === 0) {
98
- // Parse results and update suggestions
99
- const { results, suggestions } = parseGitGrepOutput(result.stdout);
100
- gitGrepResults = results;
101
-
102
- // Update prompt with suggestions
103
- editor.setPromptSuggestions(suggestions);
104
-
105
- // Update status
106
- if (results.length > 0) {
107
- editor.setStatus(editor.t("status.found", { count: String(results.length) }));
108
- } else {
109
- editor.setStatus(editor.t("status.no_matches"));
110
- }
111
- } else if (result.exit_code === 1) {
112
- // No matches found (git grep returns 1)
113
- gitGrepResults = [];
114
- editor.setPromptSuggestions([]);
115
- editor.setStatus(editor.t("status.no_matches"));
116
- } else {
117
- // Error occurred
118
- editor.setStatus(editor.t("status.error", { error: result.stderr }));
119
- }
120
- })
121
- .catch((e) => {
122
- editor.setStatus(editor.t("status.error", { error: String(e) }));
123
- });
124
-
125
- return true;
126
- };
127
-
128
- // Handle prompt confirmation (user pressed Enter)
129
- globalThis.onGitGrepPromptConfirmed = function(args: {
130
- prompt_type: string;
131
- selected_index: number | null;
132
- input: string;
133
- }): boolean {
134
- if (args.prompt_type !== "git-grep") {
135
- return true; // Not our prompt
136
- }
137
-
138
- editor.debug(
139
- `prompt-confirmed: selected_index=${args.selected_index}, num_results=${gitGrepResults.length}`
44
+ const result = await editor.spawnProcess(
45
+ "git",
46
+ ["grep", "-n", "--column", "-I", "--", query],
47
+ cwd
140
48
  );
141
49
 
142
- // Check if user selected a suggestion
143
- if (args.selected_index !== null && gitGrepResults[args.selected_index]) {
144
- const selected = gitGrepResults[args.selected_index];
145
-
146
- editor.debug(`Opening file: ${selected.file}:${selected.line}:${selected.column}`);
147
-
148
- // Open the file at the specific location
149
- editor.openFile(selected.file, selected.line, selected.column);
150
- editor.setStatus(editor.t("status.opened", { location: `${selected.file}:${selected.line}:${selected.column}` }));
151
- } else {
152
- // No selection
153
- editor.debug("No file selected - selected_index is null or out of bounds");
154
- editor.setStatus(editor.t("status.no_selection"));
155
- }
156
-
157
- return true;
158
- };
159
-
160
- // Handle prompt cancellation (user pressed Escape)
161
- globalThis.onGitGrepPromptCancelled = function(args: {
162
- prompt_type: string;
163
- }): boolean {
164
- if (args.prompt_type !== "git-grep") {
165
- return true; // Not our prompt
50
+ if (result.exit_code === 0) {
51
+ return parseGrepOutput(result.stdout, 100) as GrepMatch[];
166
52
  }
53
+ return [];
54
+ }
167
55
 
168
- // Clear results
169
- gitGrepResults = [];
170
- editor.setStatus(editor.t("status.cancelled"));
171
-
172
- return true;
56
+ // Global function to start git grep
57
+ globalThis.start_git_grep = function (): void {
58
+ finder.prompt({
59
+ title: editor.t("prompt.grep"),
60
+ source: {
61
+ mode: "search",
62
+ search: searchWithGitGrep,
63
+ debounceMs: 150,
64
+ minQueryLength: 1,
65
+ },
66
+ });
173
67
  };
174
68
 
175
- // Register event handlers
176
- editor.on("prompt_changed", "onGitGrepPromptChanged");
177
- editor.on("prompt_confirmed", "onGitGrepPromptConfirmed");
178
- editor.on("prompt_cancelled", "onGitGrepPromptCancelled");
179
-
180
69
  // Register command
181
- editor.registerCommand(
182
- "%cmd.grep",
183
- "%cmd.grep_desc",
184
- "start_git_grep",
185
- "normal"
186
- );
70
+ editor.registerCommand("%cmd.grep", "%cmd.grep_desc", "start_git_grep", "normal");
187
71
 
188
72
  // Log that plugin loaded successfully
189
- editor.debug("Git Grep plugin loaded successfully (TypeScript)");
190
- editor.debug("Usage: Call start_git_grep() or use command palette 'Git Grep'");
73
+ editor.debug("Git Grep plugin loaded (using Finder abstraction)");
191
74
  editor.setStatus(editor.t("status.ready"));
@@ -1,4 +1,4 @@
1
- /// <reference path="../types/fresh.d.ts" />
1
+ /// <reference path="./lib/fresh.d.ts" />
2
2
  const editor = getEditor();
3
3
 
4
4
 
@@ -1,4 +1,4 @@
1
- /// <reference path="../types/fresh.d.ts" />
1
+ /// <reference path="./lib/fresh.d.ts" />
2
2
  const editor = getEditor();
3
3
 
4
4
 
@@ -932,16 +932,9 @@ globalThis.git_log_copy_hash = function(): void {
932
932
  return;
933
933
  }
934
934
 
935
- // Use spawn to copy to clipboard (works on most systems)
936
- // Try xclip first (Linux), then pbcopy (macOS), then xsel
937
- editor.spawnProcess("sh", ["-c", `echo -n "${commit.hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${commit.hash}" | pbcopy 2>/dev/null || echo -n "${commit.hash}" | xsel --clipboard 2>/dev/null`])
938
- .then(() => {
939
- editor.setStatus(editor.t("status.hash_copied", { short: commit.shortHash, full: commit.hash }));
940
- })
941
- .catch(() => {
942
- // If all clipboard commands fail, just show the hash
943
- editor.setStatus(editor.t("status.hash_display", { hash: commit.hash }));
944
- });
935
+ // Copy hash to clipboard
936
+ editor.copyToClipboard(commit.hash);
937
+ editor.setStatus(editor.t("status.hash_copied", { short: commit.shortHash, full: commit.hash }));
945
938
  };
946
939
 
947
940
  // =============================================================================