Skip to content

Commit ec136f5

Browse files
committed
Save Scroll States
1 parent aef8d04 commit ec136f5

2 files changed

Lines changed: 80 additions & 62 deletions

File tree

src/components/Tiles/Editor.tsx

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import "@codingame/monaco-vscode-swift-default-extension";
1515
import "@codingame/monaco-vscode-theme-defaults-default-extension";
1616
import { platform } from "@tauri-apps/plugin-os";
1717
import { TabLike } from "../TabLike";
18-
import { useParams } from "react-router";
19-
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
2018

2119
export type WorkerLoader = () => Worker;
2220
const workerLoaders: Partial<Record<string, WorkerLoader>> = {
@@ -95,8 +93,6 @@ export default ({
9593

9694
const { mode } = useColorScheme();
9795

98-
const { path: filePath } = useParams<"path">();
99-
10096
const monacoEl = useRef(null);
10197
const [initialized, setInitialized] = useState(false);
10298
const currentTabsRef = useRef(tabs);
@@ -105,8 +101,10 @@ export default ({
105101
const openNewFileRef = useRef(openNewFile);
106102
const selectionOverrideRef = useRef<ITextEditorOptions | null>(null);
107103
const hasInitializedRef = useRef(false);
108-
109-
let [hoveredOnBtn, setHoveredOnBtn] = useState<number | null>(null);
104+
const scrollStates = useRef<{
105+
[key: string]: [number, number];
106+
}>({});
107+
const [hoveredOnBtn, setHoveredOnBtn] = useState<number | null>(null);
110108

111109
useEffect(() => {
112110
globalEditorServiceCallbacks.currentTabsRef = currentTabsRef;
@@ -116,44 +114,6 @@ export default ({
116114
globalEditorServiceCallbacks.selectionOverrideRef = selectionOverrideRef;
117115
});
118116

119-
const hasAttemptedToReadOpenFiles = useRef<string | null>(null);
120-
121-
useEffect(() => {
122-
(async () => {
123-
if (!filePath) return;
124-
const savePath = await path.join(
125-
filePath,
126-
".crosscode",
127-
"openFiles.json"
128-
);
129-
try {
130-
let text = await readTextFile(savePath);
131-
console.log(text);
132-
if (!text) return;
133-
let files = JSON.parse(text) as string[];
134-
setOpenFiles(files);
135-
} catch (e) {
136-
void e;
137-
} finally {
138-
hasAttemptedToReadOpenFiles.current = filePath;
139-
}
140-
})();
141-
}, [filePath]);
142-
143-
useEffect(() => {
144-
(async () => {
145-
if (!filePath || hasAttemptedToReadOpenFiles.current !== filePath) return;
146-
const savePath = await path.join(
147-
filePath,
148-
".crosscode",
149-
"openFiles.json"
150-
);
151-
writeTextFile(savePath, JSON.stringify(openFiles)).catch((err) => {
152-
console.error("Error writing openFiles.json:", err);
153-
});
154-
})();
155-
}, [openFiles, filePath]);
156-
157117
useEffect(() => {
158118
editorRef.current = editor;
159119
setEditorUpper(editor);
@@ -271,6 +231,18 @@ export default ({
271231
};
272232
}, [initialized]);
273233

234+
useEffect(() => {
235+
if (editor === null) return;
236+
let listener = editor.onDidScrollChange((e) => {
237+
if (focused !== undefined) {
238+
scrollStates.current[tabs[focused].file] = [e.scrollTop, e.scrollLeft];
239+
}
240+
});
241+
return () => {
242+
listener.dispose();
243+
};
244+
}, [editor, focused, tabs]);
245+
274246
useEffect(() => {
275247
if (!editor || !initialized) return;
276248
monaco.editor.setTheme(mode === "dark" ? "vs-dark" : "vs");
@@ -422,6 +394,12 @@ export default ({
422394

423395
editor.setModel(modelRef.object.textEditorModel);
424396

397+
if (scrollStates.current && scrollStates.current[filePath]) {
398+
const [scrollTop, scrollLeft] = scrollStates.current[filePath];
399+
editor.setScrollTop(scrollTop);
400+
editor.setScrollLeft(scrollLeft);
401+
}
402+
425403
// I don't love doing it like this but it seems to improve consistency over just running it directly
426404
requestAnimationFrame(() => {
427405
if (

src/pages/IDE.tsx

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Splitter, { GutterTheme, SplitDirection } from "@devbookhq/splitter";
22
import Tile from "../components/Tiles/Tile";
33
import FileExplorer from "../components/Tiles/FileExplorer";
4-
import { useCallback, useContext, useEffect, useState } from "react";
4+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
55
import Editor from "../components/Tiles/Editor";
66
import MenuBar from "../components/Menu/MenuBar";
77
import "./IDE.css";
@@ -11,6 +11,7 @@ import { useIDE } from "../utilities/IDEContext";
1111
import { registerFileSystemOverlay } from "@codingame/monaco-vscode-files-service-override";
1212
import TauriFileSystemProvider from "../utilities/TauriFileSystemProvider";
1313
import { invoke } from "@tauri-apps/api/core";
14+
import { path } from "@tauri-apps/api";
1415
import {
1516
Button,
1617
Divider,
@@ -25,6 +26,7 @@ import { restartServer } from "../utilities/lsp-client";
2526
import BottomBar from "../components/Tiles/BottomBar";
2627
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
2728
import { IStandaloneCodeEditor } from "@codingame/monaco-vscode-api/vscode/vs/editor/standalone/browser/standaloneCodeEditor";
29+
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
2830

2931
export interface IDEProps {}
3032

@@ -45,7 +47,7 @@ export default () => {
4547
const [undo, setUndo] = useState<(() => void) | null>(null);
4648
const [redo, setRedo] = useState<(() => void) | null>(null);
4749
const [theme] = useStore<"light" | "dark">("appearance/theme", "dark");
48-
const { path } = useParams<"path">();
50+
const { path: filePath } = useParams<"path">();
4951
const { openFolderDialog, selectedToolchain, hasLimitedRam, initialized } =
5052
useIDE();
5153
const [sourcekitStartup, setSourcekitStartup] = useStore<boolean | null>(
@@ -57,7 +59,7 @@ export default () => {
5759
false
5860
);
5961

60-
if (!path) {
62+
if (!filePath) {
6163
throw new Error("Path parameter is required in IDE component");
6264
}
6365

@@ -68,35 +70,73 @@ export default () => {
6870
const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null);
6971
const { addToast } = useToast();
7072

73+
const hasAttemptedToReadOpenFiles = useRef<string | null>(null);
74+
75+
useEffect(() => {
76+
(async () => {
77+
if (!filePath) return;
78+
const savePath = await path.join(
79+
filePath,
80+
".crosscode",
81+
"openFiles.json"
82+
);
83+
try {
84+
let text = await readTextFile(savePath);
85+
console.log(text);
86+
if (!text) return;
87+
let files = JSON.parse(text) as string[];
88+
setOpenFiles(files);
89+
} catch (e) {
90+
void e;
91+
} finally {
92+
hasAttemptedToReadOpenFiles.current = filePath;
93+
}
94+
})();
95+
}, [filePath]);
96+
97+
useEffect(() => {
98+
(async () => {
99+
if (!filePath || hasAttemptedToReadOpenFiles.current !== filePath) return;
100+
const savePath = await path.join(
101+
filePath,
102+
".crosscode",
103+
"openFiles.json"
104+
);
105+
writeTextFile(savePath, JSON.stringify(openFiles)).catch((err) => {
106+
console.error("Error writing openFiles.json:", err);
107+
});
108+
})();
109+
}, [openFiles, filePath]);
110+
71111
useEffect(() => {
72112
(async () => {
73-
if (!store || !storeInitialized || !path) return;
74-
await store.set("last-opened-project", encodeURIComponent(path!));
113+
if (!store || !storeInitialized || !filePath) return;
114+
await store.set("last-opened-project", encodeURIComponent(filePath!));
75115
})();
76-
}, [path, store, storeInitialized]);
116+
}, [filePath, store, storeInitialized]);
77117

78118
useEffect(() => {
79119
if (
80-
path === undefined ||
81-
path === null ||
120+
filePath === undefined ||
121+
filePath === null ||
82122
selectedToolchain === null ||
83123
!initialized
84124
)
85125
return;
86126
setProjectValidation(null);
87127
(async () => {
88-
if (path) {
128+
if (filePath) {
89129
const toolchainPath = selectedToolchain?.path ?? "";
90130
const validation = await invoke<ProjectValidation>("validate_project", {
91-
projectPath: path,
131+
projectPath: filePath,
92132
toolchainPath: toolchainPath,
93133
});
94134
if (validation) {
95135
setProjectValidation(validation);
96136
}
97137
}
98138
})();
99-
}, [path, selectedToolchain, initialized]);
139+
}, [filePath, selectedToolchain, initialized]);
100140

101141
useEffect(() => {
102142
if (openFiles.length === 0) {
@@ -110,7 +150,7 @@ export default () => {
110150
useEffect(() => {
111151
let dispose = () => {};
112152

113-
if (path) {
153+
if (filePath) {
114154
const provider = new TauriFileSystemProvider(false);
115155
const overlayDisposable = registerFileSystemOverlay(1, provider);
116156
dispose = () => {
@@ -121,7 +161,7 @@ export default () => {
121161
return () => {
122162
dispose();
123163
};
124-
}, [path]);
164+
}, [filePath]);
125165

126166
useEffect(() => {
127167
let autoEnable = async () => {
@@ -136,17 +176,17 @@ export default () => {
136176
if (!sourcekitStartup || selectedToolchain == null) return;
137177
requestAnimationFrame(async () => {
138178
try {
139-
if (autoStartedLsp === path) return;
140-
autoStartedLsp = path;
141-
await restartServer(path, selectedToolchain);
179+
if (autoStartedLsp === filePath) return;
180+
autoStartedLsp = filePath;
181+
await restartServer(filePath, selectedToolchain);
142182
} catch (e) {
143183
console.error("Failed to start SourceKit-LSP:", e);
144184
addToast.error(
145185
"Failed to start SourceKit-LSP (see devtools for details). Some language features may not be available."
146186
);
147187
}
148188
});
149-
}, [sourcekitStartup, path, selectedToolchain]);
189+
}, [sourcekitStartup, filePath, selectedToolchain]);
150190

151191
const openNewFile = useCallback((file: string) => {
152192
setOpenFile(file);
@@ -184,7 +224,7 @@ export default () => {
184224
initialSizes={[20, 80]}
185225
>
186226
<Tile className="file-explorer-tile">
187-
<FileExplorer openFolder={path} setOpenFile={openNewFile} />
227+
<FileExplorer openFolder={filePath} setOpenFile={openNewFile} />
188228
</Tile>
189229
<Splitter
190230
gutterTheme={theme === "dark" ? GutterTheme.Dark : GutterTheme.Light}

0 commit comments

Comments
 (0)