bunmicro 0.9.23 → 0.9.30
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 +15 -0
- package/README.md +10 -0
- package/package.json +3 -2
- package/src/buffer/backup.js +160 -0
- package/src/config/colorscheme.js +10 -20
- package/src/index.js +396 -68
- package/tests/backup.test.js +133 -0
- package/tests/pty-demo.js +492 -0
- package/todo.txt +6 -1
package/src/index.js
CHANGED
|
@@ -23,6 +23,8 @@ import { platformId, run as runCommand, runSync, fetchHttpBytes, detectHttpBacke
|
|
|
23
23
|
import { shellSplit } from "./shell/shell.js";
|
|
24
24
|
import { styleToAnsi } from "./display/ansi-style.js";
|
|
25
25
|
import { encodeBinaryToBuffer, decodeBinaryBytes } from "./buffer/fixed3-codec.js";
|
|
26
|
+
import { writeBackup, removeBackup, applyBackup } from "./buffer/backup.js";
|
|
27
|
+
import { createInterface } from "node:readline/promises";
|
|
26
28
|
|
|
27
29
|
import pkg from "../package.json" with { type: "json" };
|
|
28
30
|
|
|
@@ -92,6 +94,9 @@ const DEFAULT_SETTINGS = {
|
|
|
92
94
|
matchbraceleft: true,
|
|
93
95
|
matchbracestyle: "underline",
|
|
94
96
|
savecursor: false,
|
|
97
|
+
backup: true,
|
|
98
|
+
backupdir: "",
|
|
99
|
+
permbackup: false,
|
|
95
100
|
softwrap: false,
|
|
96
101
|
wordwrap: false,
|
|
97
102
|
pageoverlap: 2,
|
|
@@ -101,7 +106,11 @@ const DEFAULT_SETTINGS = {
|
|
|
101
106
|
fileformat: process.platform === "win32" ? "dos" : "unix",
|
|
102
107
|
"comment.type": "",
|
|
103
108
|
commenttype: "",
|
|
104
|
-
|
|
109
|
+
hltrailingws: false,
|
|
110
|
+
hltaberrors: false,
|
|
111
|
+
colorcolumn: 0,
|
|
112
|
+
showchars: "",
|
|
113
|
+
indentchar: " ",
|
|
105
114
|
};
|
|
106
115
|
|
|
107
116
|
const LONG_LINE_REHIGHLIGHT_LIMIT = 300;
|
|
@@ -115,6 +124,20 @@ function write(data) {
|
|
|
115
124
|
process.stdout.write(data);
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
// Pre-TUI terminal prompt — stdin must still be in line (cooked) mode.
|
|
128
|
+
// Accepts an optional input stream so the TUI path can pass its own tty fd.
|
|
129
|
+
async function termPromptLine(msg, input = process.stdin) {
|
|
130
|
+
const rl = createInterface({ input, output: process.stdout });
|
|
131
|
+
try {
|
|
132
|
+
console.log(Bun.markdown.ansi(msg))
|
|
133
|
+
return await rl.question("> ");
|
|
134
|
+
} catch {
|
|
135
|
+
return "";
|
|
136
|
+
} finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
function sgr(...codes) {
|
|
119
142
|
return `\x1b[${codes.join(";")}m`;
|
|
120
143
|
}
|
|
@@ -607,7 +630,15 @@ class BufferModel {
|
|
|
607
630
|
if (this.lines.length === 0) this.lines = [""];
|
|
608
631
|
this.cursor = { x: 0, y: 0 };
|
|
609
632
|
this.scroll = { x: 0, y: 0, row: 0 };
|
|
610
|
-
this.
|
|
633
|
+
this._modified = false;
|
|
634
|
+
this._backupRequested = false;
|
|
635
|
+
this._backupRevision = 0;
|
|
636
|
+
Object.defineProperty(this, "modified", {
|
|
637
|
+
configurable: true,
|
|
638
|
+
enumerable: true,
|
|
639
|
+
get: () => this._modified,
|
|
640
|
+
set: (value) => this.setModified(value),
|
|
641
|
+
});
|
|
611
642
|
this.readonly = readonly;
|
|
612
643
|
this.modTimeMs = modTimeMs;
|
|
613
644
|
this.reloadDisabled = false;
|
|
@@ -640,6 +671,9 @@ class BufferModel {
|
|
|
640
671
|
matchbraceleft: DEFAULT_SETTINGS.matchbraceleft,
|
|
641
672
|
matchbracestyle: DEFAULT_SETTINGS.matchbracestyle,
|
|
642
673
|
savecursor: DEFAULT_SETTINGS.savecursor,
|
|
674
|
+
backup: DEFAULT_SETTINGS.backup,
|
|
675
|
+
backupdir: DEFAULT_SETTINGS.backupdir,
|
|
676
|
+
permbackup: DEFAULT_SETTINGS.permbackup,
|
|
643
677
|
softwrap: DEFAULT_SETTINGS.softwrap,
|
|
644
678
|
wordwrap: DEFAULT_SETTINGS.wordwrap,
|
|
645
679
|
pageoverlap: DEFAULT_SETTINGS.pageoverlap,
|
|
@@ -647,7 +681,11 @@ class BufferModel {
|
|
|
647
681
|
reload: DEFAULT_SETTINGS.reload,
|
|
648
682
|
eofnewline: DEFAULT_SETTINGS.eofnewline,
|
|
649
683
|
fileformat: this.fileformat,
|
|
650
|
-
|
|
684
|
+
hltrailingws: DEFAULT_SETTINGS.hltrailingws,
|
|
685
|
+
hltaberrors: DEFAULT_SETTINGS.hltaberrors,
|
|
686
|
+
colorcolumn: DEFAULT_SETTINGS.colorcolumn,
|
|
687
|
+
showchars: DEFAULT_SETTINGS.showchars,
|
|
688
|
+
indentchar: DEFAULT_SETTINGS.indentchar,
|
|
651
689
|
encoding: this.encoding,
|
|
652
690
|
readonly,
|
|
653
691
|
};
|
|
@@ -666,6 +704,19 @@ class BufferModel {
|
|
|
666
704
|
if (command.searchRegex) this.search(command.searchRegex, command.searchAfterStart);
|
|
667
705
|
}
|
|
668
706
|
|
|
707
|
+
setModified(value = true) {
|
|
708
|
+
const next = Boolean(value);
|
|
709
|
+
const prev = this._modified;
|
|
710
|
+
this._modified = next;
|
|
711
|
+
if (next) {
|
|
712
|
+
this._backupRequested = true;
|
|
713
|
+
this._backupRevision++;
|
|
714
|
+
} else {
|
|
715
|
+
this._backupRequested = false;
|
|
716
|
+
if (prev && this._configDir) removeBackup(this, this._configDir);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
669
720
|
static async fromFile(path, command, context = {}) {
|
|
670
721
|
let text = "";
|
|
671
722
|
let readonly = false;
|
|
@@ -681,6 +732,7 @@ class BufferModel {
|
|
|
681
732
|
encoding = decoded.encoding;
|
|
682
733
|
}
|
|
683
734
|
const buffer = new BufferModel({ path, text, command, readonly, modTimeMs, encoding });
|
|
735
|
+
buffer._configDir = context?.config?.configDir ?? null;
|
|
684
736
|
attachSyntax(buffer, context, path, text);
|
|
685
737
|
return buffer;
|
|
686
738
|
}
|
|
@@ -1093,39 +1145,74 @@ class BufferModel {
|
|
|
1093
1145
|
async save(path = this.path) {
|
|
1094
1146
|
if (!path) throw new Error("No filename");
|
|
1095
1147
|
const detectSyntaxAfterSave = this.filetype === "unknown";
|
|
1148
|
+
const oldPath = this.AbsPath || this.path;
|
|
1149
|
+
const targetPath = resolve(path);
|
|
1096
1150
|
let text = this.lines.join("\n");
|
|
1151
|
+
if (this._configDir) {
|
|
1152
|
+
if (this._backupWritePromise) {
|
|
1153
|
+
try { await this._backupWritePromise; } catch {}
|
|
1154
|
+
}
|
|
1155
|
+
const backupRevision = this._backupRevision;
|
|
1156
|
+
const job = writeBackup(this, this._configDir, targetPath, { force: true });
|
|
1157
|
+
this._backupWritePromise = job;
|
|
1158
|
+
try {
|
|
1159
|
+
await job;
|
|
1160
|
+
} finally {
|
|
1161
|
+
if (this._backupWritePromise === job) this._backupWritePromise = null;
|
|
1162
|
+
}
|
|
1163
|
+
if (this._backupRevision === backupRevision) this._backupRequested = false;
|
|
1164
|
+
this._forceKeepBackup = true;
|
|
1165
|
+
}
|
|
1097
1166
|
if (this.encoding === "hex3") {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1167
|
+
try {
|
|
1168
|
+
await Bun.write(targetPath, decodeBinaryBytes(Buffer.from(text, "latin1")));
|
|
1169
|
+
} finally {
|
|
1170
|
+
this._forceKeepBackup = false;
|
|
1171
|
+
}
|
|
1172
|
+
this.path = targetPath;
|
|
1173
|
+
this.Path = targetPath;
|
|
1174
|
+
this.AbsPath = targetPath;
|
|
1175
|
+
this.name = basename(targetPath);
|
|
1103
1176
|
this.updateModTime();
|
|
1104
1177
|
this.readonly = !canWritePath(path);
|
|
1105
1178
|
this.Settings.readonly = this.readonly;
|
|
1106
1179
|
this.Type.Readonly = this.readonly;
|
|
1107
1180
|
this._savedSerial = this._undoSerial ?? 0;
|
|
1108
1181
|
this.modified = false;
|
|
1109
|
-
this.message = `Saved ${
|
|
1110
|
-
if (
|
|
1182
|
+
this.message = `Saved ${targetPath}`;
|
|
1183
|
+
if (this._configDir && oldPath !== targetPath) removeBackup(this, this._configDir, oldPath);
|
|
1184
|
+
this._updateOpenBufferPath(oldPath, targetPath);
|
|
1185
|
+
if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, targetPath, text);
|
|
1111
1186
|
return;
|
|
1112
1187
|
}
|
|
1113
1188
|
if ((this.Settings.eofnewline ?? DEFAULT_SETTINGS.eofnewline) && !text.endsWith("\n")) text += "\n";
|
|
1114
|
-
|
|
1189
|
+
try {
|
|
1190
|
+
await Bun.write(targetPath, encodeBufferTextForFile(text, this.Settings.fileformat ?? this.fileformat));
|
|
1191
|
+
} finally {
|
|
1192
|
+
this._forceKeepBackup = false;
|
|
1193
|
+
}
|
|
1115
1194
|
this.encoding = "utf-8";
|
|
1116
1195
|
this.Settings.encoding = "utf-8";
|
|
1117
|
-
this.path =
|
|
1118
|
-
this.Path =
|
|
1119
|
-
this.AbsPath =
|
|
1120
|
-
this.name = basename(
|
|
1196
|
+
this.path = targetPath;
|
|
1197
|
+
this.Path = targetPath;
|
|
1198
|
+
this.AbsPath = targetPath;
|
|
1199
|
+
this.name = basename(targetPath);
|
|
1121
1200
|
this.updateModTime();
|
|
1122
1201
|
this.readonly = !canWritePath(path);
|
|
1123
1202
|
this.Settings.readonly = this.readonly;
|
|
1124
1203
|
this.Type.Readonly = this.readonly;
|
|
1125
1204
|
this._savedSerial = this._undoSerial ?? 0;
|
|
1126
1205
|
this.modified = false;
|
|
1127
|
-
this.message = `Saved ${
|
|
1128
|
-
if (
|
|
1206
|
+
this.message = `Saved ${targetPath}`;
|
|
1207
|
+
if (this._configDir && oldPath !== targetPath) removeBackup(this, this._configDir, oldPath);
|
|
1208
|
+
this._updateOpenBufferPath(oldPath, targetPath);
|
|
1209
|
+
if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, targetPath, text);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
_updateOpenBufferPath(oldPath, newPath) {
|
|
1213
|
+
if (!this._openBufferMap) return;
|
|
1214
|
+
if (oldPath && this._openBufferMap.get(oldPath) === this) this._openBufferMap.delete(oldPath);
|
|
1215
|
+
this._openBufferMap.set(newPath, this);
|
|
1129
1216
|
}
|
|
1130
1217
|
|
|
1131
1218
|
// --- Autocomplete (BufferComplete) ---
|
|
@@ -1692,11 +1779,15 @@ class App {
|
|
|
1692
1779
|
get buffer() { return this.pane?.buffer ?? null; }
|
|
1693
1780
|
// backward-compat for the few spots that still use this.active / this.buffers
|
|
1694
1781
|
get active() { return this.activeTabIdx; }
|
|
1695
|
-
get buffers() {
|
|
1782
|
+
get buffers() {
|
|
1783
|
+
return [...new Set(this.tabs.flatMap((tab) =>
|
|
1784
|
+
tab.panes().flatMap((pane) => [pane.buffer, pane.prevBuffer]).filter(Boolean)
|
|
1785
|
+
))];
|
|
1786
|
+
}
|
|
1696
1787
|
|
|
1697
1788
|
paneForBuffer(buffer) {
|
|
1698
1789
|
for (const tab of this.tabs) {
|
|
1699
|
-
const pane = tab.panes().find((p) => p.buffer === buffer);
|
|
1790
|
+
const pane = tab.panes().find((p) => p.buffer === buffer || p.prevBuffer === buffer);
|
|
1700
1791
|
if (pane) return pane;
|
|
1701
1792
|
}
|
|
1702
1793
|
return null;
|
|
@@ -1747,6 +1838,45 @@ class App {
|
|
|
1747
1838
|
});
|
|
1748
1839
|
process.on("SIGINT", () => {}); // Ctrl+C is handled as copy in handleEvent
|
|
1749
1840
|
this.screen.init();
|
|
1841
|
+
// Update backup prompt to screen-aware version now that TUI is running.
|
|
1842
|
+
if (this.context._termPrompt) {
|
|
1843
|
+
this.context._termPrompt = async (msg) => {
|
|
1844
|
+
const tty = this._ttyStream ?? process.stdin;
|
|
1845
|
+
if (this._inputHandler) tty.removeListener("data", this._inputHandler);
|
|
1846
|
+
tty.setRawMode?.(false);
|
|
1847
|
+
this.screen.fini();
|
|
1848
|
+
process.stdout.write("\n");
|
|
1849
|
+
const answer = await termPromptLine(msg, tty);
|
|
1850
|
+
this.screen.previous = null;
|
|
1851
|
+
this.screen.init();
|
|
1852
|
+
tty.setRawMode?.(true);
|
|
1853
|
+
tty.resume(); // rl.close() pauses the stream; resume so data events fire again
|
|
1854
|
+
if (this._inputHandler) tty.on("data", this._inputHandler);
|
|
1855
|
+
return answer;
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
// Process buffers requested by edits. A successful backup is not repeated
|
|
1859
|
+
// until the buffer is modified again.
|
|
1860
|
+
const configDir = this.context?.config?.configDir;
|
|
1861
|
+
if (configDir) {
|
|
1862
|
+
this._backupTimer = setInterval(async () => {
|
|
1863
|
+
for (const buf of this.buffers) {
|
|
1864
|
+
if (buf._backupRequested && buf.modified && buf.path && buf.type === "default" &&
|
|
1865
|
+
(buf.Settings?.backup ?? DEFAULT_SETTINGS.backup) && !buf._backupWritePromise) {
|
|
1866
|
+
const revision = buf._backupRevision;
|
|
1867
|
+
const job = writeBackup(buf, configDir);
|
|
1868
|
+
buf._backupWritePromise = job;
|
|
1869
|
+
try {
|
|
1870
|
+
if (await job) {
|
|
1871
|
+
if (buf._backupRevision === revision) buf._backupRequested = false;
|
|
1872
|
+
}
|
|
1873
|
+
} catch {} finally {
|
|
1874
|
+
if (buf._backupWritePromise === job) buf._backupWritePromise = null;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}, 10_000);
|
|
1879
|
+
}
|
|
1750
1880
|
startupHighlightProgress = new StartupHighlightProgress(this);
|
|
1751
1881
|
try {
|
|
1752
1882
|
this.render();
|
|
@@ -1766,6 +1896,8 @@ class App {
|
|
|
1766
1896
|
|
|
1767
1897
|
async stop(code = 0) {
|
|
1768
1898
|
this.running = false;
|
|
1899
|
+
if (this._backupTimer) { clearInterval(this._backupTimer); this._backupTimer = null; }
|
|
1900
|
+
await Promise.allSettled(this.buffers.map((buf) => buf._backupWritePromise).filter(Boolean));
|
|
1769
1901
|
for (const tab of this.tabs)
|
|
1770
1902
|
for (const p of tab.panes())
|
|
1771
1903
|
if (p.type === "term") p.terminal?.close();
|
|
@@ -1782,6 +1914,10 @@ class App {
|
|
|
1782
1914
|
}
|
|
1783
1915
|
try { await saveCursorStates(this.context.config.configDir, this.context.cursorStates); } catch {}
|
|
1784
1916
|
}
|
|
1917
|
+
const configDir = this.context?.config?.configDir;
|
|
1918
|
+
if (configDir) {
|
|
1919
|
+
for (const buf of this.buffers) removeBackup(buf, configDir);
|
|
1920
|
+
}
|
|
1785
1921
|
process.exit(code);
|
|
1786
1922
|
}
|
|
1787
1923
|
|
|
@@ -1879,7 +2015,9 @@ class App {
|
|
|
1879
2015
|
const ft = (buf?.filetype && buf.filetype !== "unknown") ? buf.filetype : "?";
|
|
1880
2016
|
const fmt = buf?.fileformat ?? "unix";
|
|
1881
2017
|
const enc = buf?.encoding ?? "utf-8";
|
|
1882
|
-
const baseStatus =
|
|
2018
|
+
const baseStatus = this.context.colorscheme?.styles?.has("statusline")
|
|
2019
|
+
? this.context.colorscheme.get("statusline")
|
|
2020
|
+
: { ...defaultStyle, reverse: true };
|
|
1883
2021
|
const redStatus = { ...baseStatus, fg: "red" };
|
|
1884
2022
|
// Fill entire row with base style first
|
|
1885
2023
|
putText(this.screen, 0, statusRow, " ".repeat(this.cols), baseStatus, this.cols);
|
|
@@ -2010,8 +2148,9 @@ class App {
|
|
|
2010
2148
|
}
|
|
2011
2149
|
|
|
2012
2150
|
renderMessageRow(defaultStyle, row, message) {
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2151
|
+
const style = this.context.colorscheme?.styles?.has("message")
|
|
2152
|
+
? this.context.colorscheme.get("message")
|
|
2153
|
+
: defaultStyle;
|
|
2015
2154
|
putText(this.screen, 0, row, " ".repeat(this.cols), style, this.cols);
|
|
2016
2155
|
this._messageRowY = row;
|
|
2017
2156
|
this._messageRowClickZone = null;
|
|
@@ -2041,15 +2180,15 @@ class App {
|
|
|
2041
2180
|
}
|
|
2042
2181
|
|
|
2043
2182
|
renderSuggestions(defaultStyle, row, suggestions, curIdx) {
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2183
|
+
const cs = this.context.colorscheme;
|
|
2184
|
+
const baseStyle = cs?.styles?.has("statusline.suggestions")
|
|
2185
|
+
? cs.get("statusline.suggestions")
|
|
2186
|
+
: cs?.styles?.has("statusline")
|
|
2187
|
+
? cs.get("statusline")
|
|
2188
|
+
: { ...defaultStyle, reverse: true };
|
|
2048
2189
|
const selStyle = {
|
|
2049
2190
|
...baseStyle,
|
|
2050
|
-
|
|
2051
|
-
bg: baseStyle.fg === "default" ? "brightwhite" : baseStyle.fg,
|
|
2052
|
-
reverse: false,
|
|
2191
|
+
reverse: true,
|
|
2053
2192
|
};
|
|
2054
2193
|
|
|
2055
2194
|
// Compute each item's position in the virtual (pre-scroll) space.
|
|
@@ -2172,9 +2311,16 @@ class App {
|
|
|
2172
2311
|
const softwrap = buf.Settings?.softwrap ?? false;
|
|
2173
2312
|
const wordwrap = softwrap && (buf.Settings?.wordwrap ?? false);
|
|
2174
2313
|
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
2175
|
-
const gutterStyle = { ...defaultStyle, fg: "brightblack" };
|
|
2176
|
-
const dirtyGutterStyle = { ...gutterStyle, fg: "red" };
|
|
2177
2314
|
const isActivePane = pane === this.tab.activePane;
|
|
2315
|
+
const gutterStyle = this.context.colorscheme?.styles?.has("line-number")
|
|
2316
|
+
? this.context.colorscheme.get("line-number")
|
|
2317
|
+
: defaultStyle;
|
|
2318
|
+
const cursorlineOn = buf.Settings?.cursorline ?? DEFAULT_SETTINGS.cursorline;
|
|
2319
|
+
const csHasCurNum = this.context.colorscheme?.styles?.has("current-line-number");
|
|
2320
|
+
const curNumStyle = !csHasCurNum
|
|
2321
|
+
? defaultStyle
|
|
2322
|
+
: (cursorlineOn ? this.context.colorscheme.get("current-line-number") : gutterStyle);
|
|
2323
|
+
const dirtyGutterStyle = { ...gutterStyle, fg: "red" };
|
|
2178
2324
|
const useCursorline = (buf.Settings?.cursorline ?? DEFAULT_SETTINGS.cursorline) && isActivePane;
|
|
2179
2325
|
const clBg = (useCursorline && this.context.colorscheme?.styles?.has("cursor-line"))
|
|
2180
2326
|
? (this.context.colorscheme.get("cursor-line")?.fg ?? null)
|
|
@@ -2187,12 +2333,12 @@ class App {
|
|
|
2187
2333
|
const diffCol = (buf.Settings?.diffgutter ?? false) ? 1 : 0;
|
|
2188
2334
|
const lineNumW = gutterW - msgW - diffCol;
|
|
2189
2335
|
const cs = this.context.colorscheme;
|
|
2190
|
-
const diffAddStyle = cs?.
|
|
2191
|
-
const diffModStyle = cs?.
|
|
2192
|
-
const diffDelStyle = cs?.
|
|
2193
|
-
const msgInfoStyle
|
|
2194
|
-
const msgWarnStyle
|
|
2195
|
-
const msgErrStyle
|
|
2336
|
+
const diffAddStyle = cs?.styles?.has("diff-added") ? cs.get("diff-added") : null;
|
|
2337
|
+
const diffModStyle = cs?.styles?.has("diff-modified") ? cs.get("diff-modified") : null;
|
|
2338
|
+
const diffDelStyle = cs?.styles?.has("diff-deleted") ? cs.get("diff-deleted") : null;
|
|
2339
|
+
const msgInfoStyle = cs?.styles?.has("gutter-info") ? cs.get("gutter-info") : defaultStyle;
|
|
2340
|
+
const msgWarnStyle = cs?.styles?.has("gutter-warning") ? cs.get("gutter-warning") : defaultStyle;
|
|
2341
|
+
const msgErrStyle = cs?.styles?.has("gutter-error") ? cs.get("gutter-error") : defaultStyle;
|
|
2196
2342
|
|
|
2197
2343
|
const renderGutter = (lineNo, row, screenRow, subRow = 0) => {
|
|
2198
2344
|
// Message indicator: 2 cols, '> ' with kind-based style (Go: drawGutter)
|
|
@@ -2209,19 +2355,22 @@ class App {
|
|
|
2209
2355
|
}
|
|
2210
2356
|
putText(this.screen, pane.x, screenRow, msgCh + " ", msgSt, 2);
|
|
2211
2357
|
}
|
|
2358
|
+
const isCurrentLine = isActivePane && !pane.selection && lineNo === buf.cursor.y;
|
|
2359
|
+
const lineStyle = isCurrentLine ? curNumStyle : gutterStyle;
|
|
2212
2360
|
if (diffCol > 0) {
|
|
2213
2361
|
const m = diffMarks?.[lineNo] ?? 0;
|
|
2214
|
-
const [ch,
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2362
|
+
const [ch, colorStyle] = m === 1 ? ["▌", diffAddStyle]
|
|
2363
|
+
: m === 2 ? ["▌", diffModStyle]
|
|
2364
|
+
: m === 3 ? ["▔", diffDelStyle]
|
|
2365
|
+
: [" ", null];
|
|
2366
|
+
const st = colorStyle ? { ...lineStyle, fg: colorStyle.fg } : lineStyle;
|
|
2218
2367
|
putText(this.screen, pane.x + msgW, screenRow, subRow === 0 ? ch : " ", st, 1);
|
|
2219
2368
|
}
|
|
2220
2369
|
if (lineNumW > 0) {
|
|
2221
2370
|
const prefix = subRow === 0
|
|
2222
2371
|
? lineNumberText(buf, lineNo, row, lineNumW)
|
|
2223
2372
|
: visualLineNumberText(subRow, lineNumW);
|
|
2224
|
-
putText(this.screen, pane.x + msgW + diffCol, screenRow, prefix, isDirtyLongLine(buf, lineNo) ? dirtyGutterStyle :
|
|
2373
|
+
putText(this.screen, pane.x + msgW + diffCol, screenRow, prefix, isDirtyLongLine(buf, lineNo) ? dirtyGutterStyle : lineStyle, lineNumW);
|
|
2225
2374
|
}
|
|
2226
2375
|
};
|
|
2227
2376
|
|
|
@@ -2305,7 +2454,11 @@ class App {
|
|
|
2305
2454
|
|
|
2306
2455
|
renderDividers(node, defaultStyle) {
|
|
2307
2456
|
if (node instanceof Pane) return;
|
|
2308
|
-
const
|
|
2457
|
+
const divReverse = this.context.config?.globalSettings?.divreverse ?? true;
|
|
2458
|
+
const baseDiv = this.context.colorscheme?.styles?.has("divider")
|
|
2459
|
+
? this.context.colorscheme.get("divider")
|
|
2460
|
+
: defaultStyle;
|
|
2461
|
+
const divStyle = divReverse ? { ...baseDiv, reverse: true } : baseDiv;
|
|
2309
2462
|
for (let i = 0; i < node.children.length - 1; i++) {
|
|
2310
2463
|
const child = node.children[i];
|
|
2311
2464
|
if (node.dir === "h") {
|
|
@@ -2322,21 +2475,37 @@ class App {
|
|
|
2322
2475
|
}
|
|
2323
2476
|
|
|
2324
2477
|
renderTabbar(defaultStyle) {
|
|
2325
|
-
const
|
|
2326
|
-
const
|
|
2478
|
+
const cs = this.context.colorscheme;
|
|
2479
|
+
const gs = this.context.config?.globalSettings;
|
|
2480
|
+
const tabReverse = gs?.tabreverse ?? true;
|
|
2481
|
+
const tabHighlight = gs?.tabhighlight ?? false;
|
|
2482
|
+
const tabCharReverse = (tabReverse || tabHighlight) && !(tabReverse && tabHighlight);
|
|
2483
|
+
|
|
2484
|
+
const stylesFor = (reverse) => {
|
|
2485
|
+
const base = cs?.styles?.has("tabbar")
|
|
2486
|
+
? cs.get("tabbar")
|
|
2487
|
+
: { ...defaultStyle, reverse };
|
|
2488
|
+
const active = cs?.styles?.has("tabbar.active")
|
|
2489
|
+
? cs.get("tabbar.active")
|
|
2490
|
+
: base;
|
|
2491
|
+
return [base, active];
|
|
2492
|
+
};
|
|
2493
|
+
const [sepStyle] = stylesFor(tabReverse);
|
|
2494
|
+
const [charBase, charActive] = stylesFor(tabCharReverse);
|
|
2495
|
+
|
|
2327
2496
|
let x = 0;
|
|
2328
2497
|
for (let i = 0; i < this.tabs.length && x < this.cols; i++) {
|
|
2329
2498
|
const name = this.tabs[i].name || "No name";
|
|
2330
2499
|
const isActive = i === this.activeTabIdx;
|
|
2331
2500
|
const label = isActive ? `[${name}]` : ` ${name} `;
|
|
2332
|
-
const style = isActive ?
|
|
2501
|
+
const style = isActive ? charActive : charBase;
|
|
2333
2502
|
const start = x;
|
|
2334
2503
|
x = putText(this.screen, x, 0, label, style, this.cols - x);
|
|
2335
2504
|
this.tabRects.push({ index: i, start, end: x });
|
|
2336
2505
|
if (i < this.tabs.length - 1 && x < this.cols)
|
|
2337
|
-
x = putText(this.screen, x, 0, " ",
|
|
2506
|
+
x = putText(this.screen, x, 0, " ", sepStyle, this.cols - x);
|
|
2338
2507
|
}
|
|
2339
|
-
if (x < this.cols) putText(this.screen, x, 0, " ".repeat(this.cols - x),
|
|
2508
|
+
if (x < this.cols) putText(this.screen, x, 0, " ".repeat(this.cols - x), sepStyle, this.cols - x);
|
|
2340
2509
|
}
|
|
2341
2510
|
|
|
2342
2511
|
updateScrollForPane(pane) {
|
|
@@ -3818,9 +3987,11 @@ class App {
|
|
|
3818
3987
|
}
|
|
3819
3988
|
async openInPane(path) {
|
|
3820
3989
|
try {
|
|
3990
|
+
const previous = this.pane.buffer;
|
|
3821
3991
|
const buffer = await loadBufferForPath(path, this.context);
|
|
3822
3992
|
this.pane.buffer = buffer;
|
|
3823
3993
|
this.pane.selection = null;
|
|
3994
|
+
if (previous !== buffer) this._closeBufferIfUnused(previous);
|
|
3824
3995
|
await this.context.plugins?.run("onBufferOpen", buffer);
|
|
3825
3996
|
await this.context.jsPlugins?.run("onBufferOpen", buffer);
|
|
3826
3997
|
} catch (error) {
|
|
@@ -3832,8 +4003,10 @@ class App {
|
|
|
3832
4003
|
try {
|
|
3833
4004
|
const buffer = await loadBufferForPath(path, this.context);
|
|
3834
4005
|
if (isEmptyUntitledBuffer(this.buffer)) {
|
|
4006
|
+
const previous = this.pane.buffer;
|
|
3835
4007
|
this.pane.buffer = buffer;
|
|
3836
4008
|
this.pane.selection = null;
|
|
4009
|
+
if (previous !== buffer) this._closeBufferIfUnused(previous);
|
|
3837
4010
|
} else {
|
|
3838
4011
|
const tab = new Tab(new Pane(buffer));
|
|
3839
4012
|
this.tabs.push(tab);
|
|
@@ -4016,8 +4189,10 @@ class App {
|
|
|
4016
4189
|
|
|
4017
4190
|
closePane(pane) {
|
|
4018
4191
|
pane.terminal?.close();
|
|
4192
|
+
const closingBuffers = [...new Set([pane.buffer, pane.prevBuffer].filter(Boolean))];
|
|
4019
4193
|
const tab = this.tab;
|
|
4020
4194
|
tab.removePane(pane);
|
|
4195
|
+
for (const buffer of closingBuffers) this._closeBufferIfUnused(buffer);
|
|
4021
4196
|
if (!tab.root) {
|
|
4022
4197
|
// Tab is empty — close it
|
|
4023
4198
|
if (this.tabs.length <= 1) { this.stop(0); return; }
|
|
@@ -4032,10 +4207,12 @@ class App {
|
|
|
4032
4207
|
await this.stop(0);
|
|
4033
4208
|
return;
|
|
4034
4209
|
}
|
|
4210
|
+
const closingBuffers = [...new Set(this.tab.panes().flatMap((pane) => [pane.buffer, pane.prevBuffer]).filter(Boolean))];
|
|
4035
4211
|
const closing = this.buffer;
|
|
4036
4212
|
this.tabs.splice(this.activeTabIdx, 1);
|
|
4037
4213
|
this.activeTabIdx = Math.min(this.activeTabIdx, this.tabs.length - 1);
|
|
4038
4214
|
this.message = "";
|
|
4215
|
+
for (const buffer of closingBuffers) this._closeBufferIfUnused(buffer);
|
|
4039
4216
|
if (this.context.plugins && this.buffer) this.context.plugins.curPaneAdapter = makePaneAdapter(this.buffer, this);
|
|
4040
4217
|
await this.context.plugins?.run("onSetActive", makePaneAdapter(this.buffer, this));
|
|
4041
4218
|
await this.context.plugins?.run("onBufferClose", closing);
|
|
@@ -4044,6 +4221,14 @@ class App {
|
|
|
4044
4221
|
this.render();
|
|
4045
4222
|
}
|
|
4046
4223
|
|
|
4224
|
+
_closeBufferIfUnused(buffer) {
|
|
4225
|
+
if (!buffer || this.paneForBuffer(buffer)) return;
|
|
4226
|
+
const configDir = this.context?.config?.configDir;
|
|
4227
|
+
if (configDir) removeBackup(buffer, configDir);
|
|
4228
|
+
const map = this.context?._openBuffers;
|
|
4229
|
+
if (map && map.get(buffer.AbsPath) === buffer) map.delete(buffer.AbsPath);
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4047
4232
|
openCommandMode(initial = "") {
|
|
4048
4233
|
const originalColorscheme = this.context.colorscheme;
|
|
4049
4234
|
const previewTheme = async (value) => {
|
|
@@ -4401,25 +4586,27 @@ class App {
|
|
|
4401
4586
|
case "vsplit": {
|
|
4402
4587
|
let newBuf;
|
|
4403
4588
|
if (cmdArgs.length > 0) {
|
|
4404
|
-
try { newBuf = await
|
|
4589
|
+
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context); }
|
|
4405
4590
|
catch (err) { this.message = err.message; break; }
|
|
4406
4591
|
} else {
|
|
4407
4592
|
newBuf = new BufferModel({ command: {} });
|
|
4408
4593
|
attachSyntax(newBuf, this.context, "", "");
|
|
4409
4594
|
}
|
|
4410
4595
|
this.tab.split(this.pane, new Pane(newBuf), "h");
|
|
4596
|
+
this.render();
|
|
4411
4597
|
break;
|
|
4412
4598
|
}
|
|
4413
4599
|
case "hsplit": {
|
|
4414
4600
|
let newBuf;
|
|
4415
4601
|
if (cmdArgs.length > 0) {
|
|
4416
|
-
try { newBuf = await
|
|
4602
|
+
try { newBuf = await loadBufferForPath(resolve(expandHome(cmdArgs[0])), this.context); }
|
|
4417
4603
|
catch (err) { this.message = err.message; break; }
|
|
4418
4604
|
} else {
|
|
4419
4605
|
newBuf = new BufferModel({ command: {} });
|
|
4420
4606
|
attachSyntax(newBuf, this.context, "", "");
|
|
4421
4607
|
}
|
|
4422
4608
|
this.tab.split(this.pane, new Pane(newBuf), "v");
|
|
4609
|
+
this.render();
|
|
4423
4610
|
break;
|
|
4424
4611
|
}
|
|
4425
4612
|
case "term": {
|
|
@@ -5690,9 +5877,26 @@ async function loadBufferForPath(pathOrUrl, context, command = {}) {
|
|
|
5690
5877
|
encoding = decoded.encoding;
|
|
5691
5878
|
const urlPath = pathOrUrl.replace(/[?#].*$/, "");
|
|
5692
5879
|
buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
|
|
5880
|
+
buffer._configDir = context?.config?.configDir ?? null;
|
|
5693
5881
|
attachSyntax(buffer, context, urlPath, text);
|
|
5694
5882
|
} else {
|
|
5695
|
-
|
|
5883
|
+
if (!context._openBuffers) context._openBuffers = new Map();
|
|
5884
|
+
const absPath = resolve(pathOrUrl);
|
|
5885
|
+
const existing = context._openBuffers.get(absPath);
|
|
5886
|
+
if (existing) return existing;
|
|
5887
|
+
buffer = await BufferModel.fromFile(absPath, command, context);
|
|
5888
|
+
// Check for crash-recovery backup before returning the buffer.
|
|
5889
|
+
const promptFn = context._termPrompt;
|
|
5890
|
+
if (promptFn && buffer._configDir) {
|
|
5891
|
+
const { recovered, abort } = await applyBackup(buffer, buffer._configDir, promptFn);
|
|
5892
|
+
if (abort) return new BufferModel({ command });
|
|
5893
|
+
if (recovered) {
|
|
5894
|
+
buffer.ensureCursor();
|
|
5895
|
+
attachSyntax(buffer, context, absPath, buffer.lines.join("\n"));
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
buffer._openBufferMap = context._openBuffers;
|
|
5899
|
+
context._openBuffers.set(absPath, buffer);
|
|
5696
5900
|
}
|
|
5697
5901
|
if (DEFAULT_SETTINGS.savecursor && !commandHasStartupJump(command) && context?.cursorStates?.[pathOrUrl]) {
|
|
5698
5902
|
const saved = context.cursorStates[pathOrUrl];
|
|
@@ -6047,22 +6251,79 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6047
6251
|
// Go: cursor-line bg is skipped when a syntax style already has a non-default background (preservebg)
|
|
6048
6252
|
const defBg = colorscheme?.defaultStyle?.bg ?? "default";
|
|
6049
6253
|
|
|
6050
|
-
const showTrailingWs = buf.Settings?.
|
|
6254
|
+
const showTrailingWs = buf.Settings?.hltrailingws ?? false;
|
|
6051
6255
|
let trailingWsIdx = raw.length;
|
|
6052
6256
|
if (showTrailingWs) {
|
|
6053
6257
|
let k = raw.length - 1;
|
|
6054
6258
|
while (k >= 0 && (raw[k] === " " || raw[k] === "\t")) k--;
|
|
6055
6259
|
trailingWsIdx = k + 1;
|
|
6056
6260
|
}
|
|
6057
|
-
const
|
|
6058
|
-
?
|
|
6261
|
+
const trailingWsColor = showTrailingWs && colorscheme?.styles?.has("trailingws")
|
|
6262
|
+
? colorscheme.get("trailingws")?.fg
|
|
6263
|
+
: null;
|
|
6264
|
+
|
|
6265
|
+
// showchars parsing (Go bufwindow.go:455-476)
|
|
6266
|
+
const indentchar = buf.Settings?.indentchar ?? " ";
|
|
6267
|
+
let spacechars = " ";
|
|
6268
|
+
let tabchars = indentchar;
|
|
6269
|
+
let indentspacechars = "";
|
|
6270
|
+
let indenttabchars = "";
|
|
6271
|
+
for (const entry of String(buf.Settings?.showchars ?? "").split(",")) {
|
|
6272
|
+
const eq = entry.indexOf("=");
|
|
6273
|
+
if (eq < 0) continue;
|
|
6274
|
+
const key = entry.slice(0, eq);
|
|
6275
|
+
const val = entry.slice(eq + 1);
|
|
6276
|
+
if (key === "space") spacechars = val;
|
|
6277
|
+
else if (key === "tab") tabchars = val;
|
|
6278
|
+
else if (key === "ispace") indentspacechars = val;
|
|
6279
|
+
else if (key === "itab") indenttabchars = val;
|
|
6280
|
+
}
|
|
6281
|
+
// Only inspect visible leading whitespace. Once horizontally scrolled, the
|
|
6282
|
+
// line start is off-screen and should not make redraw cost depend on it.
|
|
6283
|
+
let leadingwsEnd = 0;
|
|
6284
|
+
if (scrollX === 0) {
|
|
6285
|
+
const visibleEnd = Math.min(raw.length, maxWidth);
|
|
6286
|
+
while (leadingwsEnd < visibleEnd && (raw[leadingwsEnd] === " " || raw[leadingwsEnd] === "\t")) leadingwsEnd++;
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
const hltaberrors = buf.Settings?.hltaberrors ?? false;
|
|
6290
|
+
const tabstospaces = buf.Settings?.tabstospaces ?? false;
|
|
6291
|
+
const tabErrorFg = hltaberrors && colorscheme?.styles?.has("tab-error")
|
|
6292
|
+
? colorscheme.get("tab-error")?.fg
|
|
6293
|
+
: null;
|
|
6294
|
+
const indentCharFg = colorscheme?.styles?.has("indent-char")
|
|
6295
|
+
? colorscheme.get("indent-char")?.fg
|
|
6059
6296
|
: null;
|
|
6297
|
+
const colorcolumn = Number(buf.Settings?.colorcolumn ?? 0) | 0;
|
|
6298
|
+
const colorColumnBg = colorcolumn > 0 && colorscheme?.styles?.has("color-column")
|
|
6299
|
+
? colorscheme.get("color-column")?.fg
|
|
6300
|
+
: null;
|
|
6301
|
+
const tabsize = buf.Settings?.tabsize ?? DEFAULT_SETTINGS.tabsize;
|
|
6302
|
+
|
|
6303
|
+
// Keep horizontal rendering bounded to the visible range. Reconstructing
|
|
6304
|
+
// the exact display width before scrollX makes long-line redraws O(scrollX).
|
|
6305
|
+
const scrollVisualCol = scrollX;
|
|
6306
|
+
|
|
6307
|
+
// Linter messages overlapping this line (Go bufwindow.go:662-668)
|
|
6308
|
+
const lineMessages = (buf.Messages ?? []).filter((m) => {
|
|
6309
|
+
const sy = m.Start?.Y ?? 0, ey = m.End?.Y ?? 0;
|
|
6310
|
+
return sy <= lineNo && ey >= lineNo;
|
|
6311
|
+
});
|
|
6312
|
+
const inMessageAt = (charIdx) => {
|
|
6313
|
+
for (const m of lineMessages) {
|
|
6314
|
+
const sY = m.Start?.Y ?? 0, sX = m.Start?.X ?? 0;
|
|
6315
|
+
const eY = m.End?.Y ?? 0, eX = m.End?.X ?? 0;
|
|
6316
|
+
const ge = lineNo > sY || (lineNo === sY && charIdx >= sX);
|
|
6317
|
+
const lt = lineNo < eY || (lineNo === eY && charIdx < eX);
|
|
6318
|
+
if (ge && lt) return true;
|
|
6319
|
+
}
|
|
6320
|
+
return false;
|
|
6321
|
+
};
|
|
6060
6322
|
|
|
6061
6323
|
let changeIndex = 0;
|
|
6062
6324
|
let searchIdx = 0;
|
|
6063
6325
|
let i = scrollX;
|
|
6064
6326
|
while (i < raw.length && width < maxWidth) {
|
|
6065
|
-
const cp = raw.codePointAt(i);
|
|
6066
6327
|
const unit = displayUnitAt(raw, i);
|
|
6067
6328
|
const ch = unit.text;
|
|
6068
6329
|
const charLen = unit.length;
|
|
@@ -6071,16 +6332,30 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6071
6332
|
while (changeIndex + 1 < changes.length && i >= changes[changeIndex + 1][0]) changeIndex++;
|
|
6072
6333
|
const group = changes[changeIndex]?.[1] ?? "default";
|
|
6073
6334
|
const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
|
|
6074
|
-
|
|
6075
|
-
|
|
6335
|
+
let preservebg = syntaxStyle.bg !== undefined && syntaxStyle.bg !== defBg;
|
|
6336
|
+
let baseStyle = (cursorLineBg && !preservebg) ? { ...syntaxStyle, bg: cursorLineBg } : syntaxStyle;
|
|
6076
6337
|
while (searchIdx < searchRanges.length && searchRanges[searchIdx][1] <= i) searchIdx++;
|
|
6077
6338
|
const inSearch = searchIdx < searchRanges.length && i >= searchRanges[searchIdx][0] && i < searchRanges[searchIdx][1];
|
|
6078
6339
|
const selected = isSelected(selection, lineNo, i, i + charLen);
|
|
6079
6340
|
const braceMatched = braceMatches?.has(String(lineNo) + ":" + String(i));
|
|
6080
|
-
|
|
6341
|
+
|
|
6342
|
+
// tab-error in leading whitespace
|
|
6343
|
+
let style = baseStyle;
|
|
6344
|
+
const inLeading = i < leadingwsEnd;
|
|
6345
|
+
if (tabErrorFg != null && inLeading) {
|
|
6346
|
+
if ((tabstospaces && ch === "\t") || (!tabstospaces && ch === " ")) {
|
|
6347
|
+
style = { ...style, bg: tabErrorFg };
|
|
6348
|
+
preservebg = true;
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
if (trailingWsColor != null && i >= trailingWsIdx) {
|
|
6352
|
+
style = { ...style, bg: trailingWsColor };
|
|
6353
|
+
preservebg = true;
|
|
6354
|
+
}
|
|
6081
6355
|
if (inSearch) {
|
|
6082
6356
|
const searchStyle = colorscheme?.styles?.has("hlsearch") ? colorscheme.get("hlsearch") : null;
|
|
6083
|
-
style = searchStyle ?? { ...
|
|
6357
|
+
style = searchStyle ?? { ...(colorscheme?.defaultStyle ?? {}), reverse: true };
|
|
6358
|
+
if ((style.bg ?? "default") !== defBg) preservebg = true;
|
|
6084
6359
|
}
|
|
6085
6360
|
if (braceMatched) {
|
|
6086
6361
|
if ((buf.Settings?.matchbracestyle ?? DEFAULT_SETTINGS.matchbracestyle) === "highlight") {
|
|
@@ -6091,22 +6366,73 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
6091
6366
|
}
|
|
6092
6367
|
}
|
|
6093
6368
|
if (selected) {
|
|
6094
|
-
|
|
6369
|
+
const selectionStyle = colorscheme?.styles?.has("selection") ? colorscheme.get("selection") : null;
|
|
6370
|
+
style = selectionStyle ?? { ...(colorscheme?.defaultStyle ?? {}), reverse: true };
|
|
6371
|
+
}
|
|
6372
|
+
if (lineMessages.length > 0 && inMessageAt(i)) {
|
|
6373
|
+
style = { ...style, underline: true };
|
|
6374
|
+
}
|
|
6375
|
+
|
|
6376
|
+
// Visualize whitespace
|
|
6377
|
+
let displayCh = ch;
|
|
6378
|
+
let useIndentCharFg = false;
|
|
6379
|
+
if (ch === " ") {
|
|
6380
|
+
// Go bufwindow.go:554-559: ispace only kicks in at tabsize boundaries (indent guide).
|
|
6381
|
+
const visualCol = scrollVisualCol + width;
|
|
6382
|
+
const useIspace = inLeading && indentspacechars && (visualCol % tabsize === 0);
|
|
6383
|
+
const candidate = useIspace ? indentspacechars : spacechars;
|
|
6384
|
+
if (candidate && candidate !== " ") {
|
|
6385
|
+
displayCh = candidate[0] ?? " ";
|
|
6386
|
+
useIndentCharFg = true;
|
|
6387
|
+
}
|
|
6388
|
+
} else if (ch === "\t") {
|
|
6389
|
+
const candidate = (inLeading && indenttabchars) ? indenttabchars : tabchars;
|
|
6390
|
+
if (candidate && candidate !== " ") {
|
|
6391
|
+
displayCh = candidate[0] ?? " ";
|
|
6392
|
+
useIndentCharFg = true;
|
|
6393
|
+
} else {
|
|
6394
|
+
displayCh = " ";
|
|
6395
|
+
}
|
|
6095
6396
|
}
|
|
6397
|
+
if (useIndentCharFg && indentCharFg != null) {
|
|
6398
|
+
style = { ...style, fg: indentCharFg };
|
|
6399
|
+
}
|
|
6400
|
+
|
|
6401
|
+
const ccAt = (visualCol) => {
|
|
6402
|
+
if (colorColumnBg == null || preservebg) return null;
|
|
6403
|
+
return visualCol === colorcolumn ? colorColumnBg : null;
|
|
6404
|
+
};
|
|
6405
|
+
|
|
6096
6406
|
if (ch === "\t") {
|
|
6097
|
-
const spaces = Math.min(
|
|
6098
|
-
for (let j = 0; j < spaces; j++)
|
|
6099
|
-
|
|
6407
|
+
const spaces = Math.min(tabsize, maxWidth - width);
|
|
6408
|
+
for (let j = 0; j < spaces; j++) {
|
|
6409
|
+
const visualCol = scrollVisualCol + width;
|
|
6410
|
+
const cellCh = j === 0 ? displayCh : " ";
|
|
6411
|
+
const ccBg = ccAt(visualCol);
|
|
6412
|
+
const cellStyle = ccBg != null ? { ...style, bg: ccBg } : style;
|
|
6413
|
+
cells.push({ ch: cellCh, style: cellStyle });
|
|
6414
|
+
width++;
|
|
6415
|
+
}
|
|
6100
6416
|
} else if (w > 0 && width + w <= maxWidth) {
|
|
6101
|
-
|
|
6417
|
+
const visualCol = scrollVisualCol + width;
|
|
6418
|
+
const ccBg = ccAt(visualCol);
|
|
6419
|
+
const cellStyle = ccBg != null ? { ...style, bg: ccBg } : style;
|
|
6420
|
+
cells.push({ ch: displayCh, style: cellStyle, width: w });
|
|
6102
6421
|
width += w;
|
|
6103
6422
|
}
|
|
6104
6423
|
i += charLen;
|
|
6105
6424
|
}
|
|
6106
|
-
//
|
|
6107
|
-
|
|
6108
|
-
const
|
|
6109
|
-
|
|
6425
|
+
// Trailing fill: cursor-line bg and color-column always apply (Go bufwindow.go:807-826)
|
|
6426
|
+
while (width < maxWidth) {
|
|
6427
|
+
const visualCol = scrollVisualCol + width;
|
|
6428
|
+
let padStyle = cursorLineBg
|
|
6429
|
+
? { ...(colorscheme?.defaultStyle ?? {}), bg: cursorLineBg }
|
|
6430
|
+
: (colorscheme?.defaultStyle ?? {});
|
|
6431
|
+
if (colorColumnBg != null && visualCol === colorcolumn) {
|
|
6432
|
+
padStyle = { ...padStyle, bg: colorColumnBg };
|
|
6433
|
+
}
|
|
6434
|
+
cells.push({ ch: " ", style: padStyle });
|
|
6435
|
+
width++;
|
|
6110
6436
|
}
|
|
6111
6437
|
return cells;
|
|
6112
6438
|
}
|
|
@@ -6373,6 +6699,8 @@ async function main() {
|
|
|
6373
6699
|
if (DEFAULT_SETTINGS.savecursor) {
|
|
6374
6700
|
context.cursorStates = await loadCursorStates(config.configDir);
|
|
6375
6701
|
}
|
|
6702
|
+
// Backup prompt available before App starts (stdin still in cooked mode).
|
|
6703
|
+
context._termPrompt = process.stdout.isTTY ? termPromptLine : null;
|
|
6376
6704
|
loadBuffers.context = context;
|
|
6377
6705
|
const buffers = await loadBuffers(files.map((file) =>
|
|
6378
6706
|
isHttpUrl(file) ? file : resolve(file)
|