Skip to content

Commit f719bf7

Browse files
authored
feat: add terminal like search and replace history (#1467)
and basic auto encoding detection (#787)
1 parent 017a83f commit f719bf7

File tree

4 files changed

+362
-9
lines changed

4 files changed

+362
-9
lines changed

src/handlers/quickTools.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import quickTools from "components/quickTools";
22
import actionStack from "lib/actionStack";
3+
import searchHistory from "lib/searchHistory";
34
import appSettings from "lib/settings";
45
import searchSettings from "settings/searchSettings";
56
import KeyboardEvent from "utils/keyboardEvent";
@@ -75,6 +76,67 @@ appSettings.on("update:quicktoolsItems:after", () => {
7576
}, 100);
7677
});
7778

79+
// Initialize history navigation
80+
function setupHistoryNavigation() {
81+
const { $searchInput, $replaceInput } = quickTools;
82+
83+
// Search input history navigation
84+
if ($searchInput.el) {
85+
$searchInput.el.addEventListener("keydown", (e) => {
86+
if (e.key === "ArrowUp") {
87+
e.preventDefault();
88+
const newValue = searchHistory.navigateSearchUp($searchInput.el.value);
89+
$searchInput.el.value = newValue;
90+
// Trigger search
91+
if (newValue) find(0, false);
92+
} else if (e.key === "ArrowDown") {
93+
e.preventDefault();
94+
const newValue = searchHistory.navigateSearchDown(
95+
$searchInput.el.value,
96+
);
97+
$searchInput.el.value = newValue;
98+
// Trigger search
99+
if (newValue) find(0, false);
100+
} else if (e.key === "Enter" || e.key === "Escape") {
101+
// Reset navigation on enter or escape
102+
searchHistory.resetSearchNavigation();
103+
}
104+
});
105+
106+
// Reset navigation when user starts typing
107+
$searchInput.el.addEventListener("input", () => {
108+
searchHistory.resetSearchNavigation();
109+
});
110+
}
111+
112+
// Replace input history navigation
113+
if ($replaceInput.el) {
114+
$replaceInput.el.addEventListener("keydown", (e) => {
115+
if (e.key === "ArrowUp") {
116+
e.preventDefault();
117+
const newValue = searchHistory.navigateReplaceUp(
118+
$replaceInput.el.value,
119+
);
120+
$replaceInput.el.value = newValue;
121+
} else if (e.key === "ArrowDown") {
122+
e.preventDefault();
123+
const newValue = searchHistory.navigateReplaceDown(
124+
$replaceInput.el.value,
125+
);
126+
$replaceInput.el.value = newValue;
127+
} else if (e.key === "Enter" || e.key === "Escape") {
128+
// Reset navigation on enter or escape
129+
searchHistory.resetReplaceNavigation();
130+
}
131+
});
132+
133+
// Reset navigation when user starts typing
134+
$replaceInput.el.addEventListener("input", () => {
135+
searchHistory.resetReplaceNavigation();
136+
});
137+
}
138+
}
139+
78140
export const key = {
79141
get shift() {
80142
return state.shift;
@@ -169,10 +231,16 @@ export default function actions(action, value) {
169231
return true;
170232

171233
case "search-prev":
234+
if (quickTools.$searchInput.el.value) {
235+
searchHistory.addToHistory(quickTools.$searchInput.el.value);
236+
}
172237
find(1, true);
173238
return true;
174239

175240
case "search-next":
241+
if (quickTools.$searchInput.el.value) {
242+
searchHistory.addToHistory(quickTools.$searchInput.el.value);
243+
}
176244
find(1, false);
177245
return true;
178246

@@ -181,10 +249,16 @@ export default function actions(action, value) {
181249
return true;
182250

183251
case "search-replace":
252+
if ($replaceInput.value) {
253+
searchHistory.addToHistory($replaceInput.value);
254+
}
184255
editor.replace($replaceInput.value || "");
185256
return true;
186257

187258
case "search-replace-all":
259+
if ($replaceInput.value) {
260+
searchHistory.addToHistory($replaceInput.value);
261+
}
188262
editor.replaceAll($replaceInput.value || "");
189263
return true;
190264

@@ -227,9 +301,15 @@ function toggleSearch() {
227301
};
228302

229303
$searchInput.onsearch = function () {
230-
if (this.value) find(1, false);
304+
if (this.value) {
305+
searchHistory.addToHistory(this.value);
306+
find(1, false);
307+
}
231308
};
232309

310+
// Setup history navigation for search inputs
311+
setupHistoryNavigation();
312+
233313
setFooterHeight(2);
234314
find(0, false);
235315

@@ -327,6 +407,10 @@ function removeSearch() {
327407
$footer.removeAttribute("data-searching");
328408
$searchRow1.remove();
329409
$searchRow2.remove();
410+
411+
// Reset history navigation when search is closed
412+
searchHistory.resetAllNavigation();
413+
330414
const { activeFile } = editorManager;
331415

332416
// Check if current tab is a terminal

src/lib/openFile.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import alert from "dialogs/alert";
44
import confirm from "dialogs/confirm";
55
import loader from "dialogs/loader";
66
import { reopenWithNewEncoding } from "palettes/changeEncoding";
7-
import { decode } from "utils/encodings";
7+
import { decode, detectEncoding } from "utils/encodings";
88
import helpers from "utils/helpers";
99
import EditorFile from "./editorFile";
1010
import fileTypeHandler from "./fileTypeHandler";
@@ -84,7 +84,7 @@ export default async function openFile(file, options = {}) {
8484
const fileInfo = await fs.stat();
8585
const name = fileInfo.name || file.filename || uri;
8686
const readOnly = fileInfo.canWrite ? false : true;
87-
const createEditor = (isUnsaved, text) => {
87+
const createEditor = (isUnsaved, text, detectedEncoding) => {
8888
new EditorFile(name, {
8989
uri,
9090
text,
@@ -93,7 +93,7 @@ export default async function openFile(file, options = {}) {
9393
render,
9494
onsave,
9595
readOnly,
96-
encoding,
96+
encoding: detectedEncoding || encoding,
9797
SAFMode: mode,
9898
});
9999
};
@@ -385,12 +385,21 @@ export default async function openFile(file, options = {}) {
385385
}
386386

387387
const binData = await fs.readFile();
388-
const fileContent = await decode(
389-
binData,
390-
file.encoding || appSettings.value.defaultFileEncoding,
391-
);
392388

393-
createEditor(false, fileContent);
389+
// Detect encoding if not explicitly provided
390+
let detectedEncoding = file.encoding || encoding;
391+
if (!detectedEncoding) {
392+
try {
393+
detectedEncoding = await detectEncoding(binData);
394+
} catch (error) {
395+
console.warn("Encoding detection failed, using default:", error);
396+
detectedEncoding = appSettings.value.defaultFileEncoding;
397+
}
398+
}
399+
400+
const fileContent = await decode(binData, detectedEncoding);
401+
402+
createEditor(false, fileContent, detectedEncoding);
394403
if (mode !== "single") recents.addFile(uri);
395404
return;
396405
} catch (error) {

src/lib/searchHistory.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Search and Replace History Manager
3+
* Manages search/replace history using localStorage
4+
*/
5+
6+
const HISTORY_KEY = "acode.searchreplace.history";
7+
const MAX_HISTORY_ITEMS = 20;
8+
9+
class SearchHistory {
10+
constructor() {
11+
this.history = this.loadHistory(HISTORY_KEY);
12+
this.searchIndex = -1; // Current position in history for search input
13+
this.replaceIndex = -1; // Current position in history for replace input
14+
this.tempSearchValue = ""; // Temporary storage for current search input
15+
this.tempReplaceValue = ""; // Temporary storage for current replace input
16+
}
17+
18+
/**
19+
* Load history from localStorage
20+
* @param {string} key Storage key
21+
* @returns {Array<string>} History items
22+
*/
23+
loadHistory(key) {
24+
try {
25+
const stored = localStorage.getItem(key);
26+
return stored ? JSON.parse(stored) : [];
27+
} catch (error) {
28+
console.warn("Failed to load search history:", error);
29+
return [];
30+
}
31+
}
32+
33+
/**
34+
* Save history to localStorage
35+
*/
36+
saveHistory() {
37+
try {
38+
localStorage.setItem(HISTORY_KEY, JSON.stringify(this.history));
39+
} catch (error) {
40+
console.warn("Failed to save search history:", error);
41+
}
42+
}
43+
44+
/**
45+
* Add item to history
46+
* @param {string} item Item to add
47+
*/
48+
addToHistory(item) {
49+
if (!item || typeof item !== "string" || item.trim().length === 0) {
50+
return;
51+
}
52+
53+
const trimmedItem = item.trim();
54+
55+
// Remove existing item if present
56+
this.history = this.history.filter((h) => h !== trimmedItem);
57+
58+
// Add to beginning
59+
this.history.unshift(trimmedItem);
60+
61+
// Limit history size
62+
this.history = this.history.slice(0, MAX_HISTORY_ITEMS);
63+
64+
this.saveHistory();
65+
}
66+
67+
/**
68+
* Get history
69+
* @returns {Array<string>} History items
70+
*/
71+
getHistory() {
72+
return [...this.history];
73+
}
74+
75+
/**
76+
* Clear all history
77+
*/
78+
clearHistory() {
79+
this.history = [];
80+
this.saveHistory();
81+
}
82+
83+
/**
84+
* Navigate up in search history (terminal-like)
85+
* @param {string} currentValue Current input value
86+
* @returns {string} Previous history item or current value
87+
*/
88+
navigateSearchUp(currentValue) {
89+
if (this.history.length === 0) return currentValue;
90+
91+
// Store current value if we're at the beginning
92+
if (this.searchIndex === -1) {
93+
this.tempSearchValue = currentValue;
94+
this.searchIndex = this.history.length - 1;
95+
} else if (this.searchIndex > 0) {
96+
this.searchIndex--;
97+
}
98+
99+
return this.history[this.searchIndex] || currentValue;
100+
}
101+
102+
/**
103+
* Navigate down in search history (terminal-like)
104+
* @param {string} currentValue Current input value
105+
* @returns {string} Next history item or original value
106+
*/
107+
navigateSearchDown(currentValue) {
108+
if (this.history.length === 0 || this.searchIndex === -1) {
109+
return currentValue;
110+
}
111+
112+
this.searchIndex++;
113+
114+
// If we've gone past the end, return to original value
115+
if (this.searchIndex >= this.history.length) {
116+
this.searchIndex = -1;
117+
return this.tempSearchValue;
118+
}
119+
120+
return this.history[this.searchIndex];
121+
}
122+
123+
/**
124+
* Navigate up in replace history (terminal-like)
125+
* @param {string} currentValue Current input value
126+
* @returns {string} Previous history item or current value
127+
*/
128+
navigateReplaceUp(currentValue) {
129+
if (this.history.length === 0) return currentValue;
130+
131+
// Store current value if we're at the beginning
132+
if (this.replaceIndex === -1) {
133+
this.tempReplaceValue = currentValue;
134+
this.replaceIndex = this.history.length - 1;
135+
} else if (this.replaceIndex > 0) {
136+
this.replaceIndex--;
137+
}
138+
139+
return this.history[this.replaceIndex] || currentValue;
140+
}
141+
142+
/**
143+
* Navigate down in replace history (terminal-like)
144+
* @param {string} currentValue Current input value
145+
* @returns {string} Next history item or original value
146+
*/
147+
navigateReplaceDown(currentValue) {
148+
if (this.history.length === 0 || this.replaceIndex === -1) {
149+
return currentValue;
150+
}
151+
152+
this.replaceIndex++;
153+
154+
// If we've gone past the end, return to original value
155+
if (this.replaceIndex >= this.history.length) {
156+
this.replaceIndex = -1;
157+
return this.tempReplaceValue;
158+
}
159+
160+
return this.history[this.replaceIndex];
161+
}
162+
163+
/**
164+
* Reset search history navigation
165+
*/
166+
resetSearchNavigation() {
167+
this.searchIndex = -1;
168+
this.tempSearchValue = "";
169+
}
170+
171+
/**
172+
* Reset replace history navigation
173+
*/
174+
resetReplaceNavigation() {
175+
this.replaceIndex = -1;
176+
this.tempReplaceValue = "";
177+
}
178+
179+
/**
180+
* Reset all navigation state
181+
*/
182+
resetAllNavigation() {
183+
this.resetSearchNavigation();
184+
this.resetReplaceNavigation();
185+
}
186+
}
187+
188+
export default new SearchHistory();

0 commit comments

Comments
 (0)