Skip to content

Commit d22b8e8

Browse files
committed
Format on save & open last focused file
1 parent ec136f5 commit d22b8e8

7 files changed

Lines changed: 216 additions & 174 deletions

File tree

bun.lock

Lines changed: 109 additions & 103 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"] }
1616
serde_json = "1"
1717
tauri-plugin-shell = "2.3.0"
1818
tauri-plugin-fs = { version = "2.4.2", features= ["watch"] }
19-
tauri-plugin-dialog = "2.3.3"
19+
tauri-plugin-dialog = "2.4.0"
2020
tauri-plugin-store = "2.4.0"
2121
tauri-plugin-opener = "2.5.0"
2222
tauri-plugin-os = "2.3.1"

src/components/Tiles/Editor.tsx

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ 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 { useStore } from "../../utilities/StoreContext";
19+
import { useParams } from "react-router";
20+
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
1821

1922
export type WorkerLoader = () => Worker;
2023
const workerLoaders: Partial<Record<string, WorkerLoader>> = {
@@ -47,7 +50,7 @@ export interface EditorProps {
4750
openFiles: string[];
4851
setOpenFiles: Dispatch<SetStateAction<string[]>>;
4952
focusedFile: string | null;
50-
setSaveFile: Dispatch<SetStateAction<(() => void) | null>>;
53+
setSaveFile: Dispatch<SetStateAction<(() => Promise<void>) | null>>;
5154
setUndo: Dispatch<SetStateAction<(() => void) | null>>;
5255
setRedo: Dispatch<SetStateAction<(() => void) | null>>;
5356
openNewFile: (file: string) => void;
@@ -105,6 +108,65 @@ export default ({
105108
[key: string]: [number, number];
106109
}>({});
107110
const [hoveredOnBtn, setHoveredOnBtn] = useState<number | null>(null);
111+
const [formatOnSave] = useStore<boolean>("sourcekit/format", true);
112+
113+
const { path: filePath } = useParams<"path">();
114+
const hasAttemptedToReadOpenFiles = useRef<string | null>(null);
115+
116+
useEffect(() => {
117+
(async () => {
118+
if (!filePath || hasAttemptedToReadOpenFiles.current === filePath) return;
119+
const savePath = await path.join(
120+
filePath,
121+
".crosscode",
122+
"openFiles.json"
123+
);
124+
try {
125+
let text = await readTextFile(savePath);
126+
if (!text) return;
127+
let data = JSON.parse(text) as { files: string[]; focused: number };
128+
// make sure that types are as expected
129+
if (
130+
!data ||
131+
typeof data !== "object" ||
132+
!Array.isArray(data.files) ||
133+
typeof data.focused !== "number"
134+
)
135+
return;
136+
data.files = data.files.filter((file) => typeof file === "string");
137+
if (data.files.length === 0) return;
138+
if (data.focused < 0 || data.focused >= data.files.length) {
139+
data.focused = 0;
140+
}
141+
setOpenFiles(data.files);
142+
setTimeout(() => {
143+
setFocused(data.focused);
144+
}, 100);
145+
} catch (e) {
146+
void e;
147+
} finally {
148+
hasAttemptedToReadOpenFiles.current = filePath;
149+
}
150+
})();
151+
}, [filePath, setOpenFiles, setFocused]);
152+
153+
useEffect(() => {
154+
(async () => {
155+
if (!filePath || hasAttemptedToReadOpenFiles.current !== filePath) return;
156+
const savePath = await path.join(
157+
filePath,
158+
".crosscode",
159+
"openFiles.json"
160+
);
161+
let data = {
162+
files: openFiles,
163+
focused: focused ?? 0,
164+
};
165+
writeTextFile(savePath, JSON.stringify(data)).catch((err) => {
166+
console.error("Error writing openFiles.json:", err);
167+
});
168+
})();
169+
}, [openFiles, filePath, focused]);
108170

109171
useEffect(() => {
110172
globalEditorServiceCallbacks.currentTabsRef = currentTabsRef;
@@ -390,7 +452,13 @@ export default ({
390452
}
391453
});
392454
});
393-
setSaveFile(() => modelRef.object.save.bind(modelRef.object));
455+
456+
setSaveFile(() => async () => {
457+
if (formatOnSave) {
458+
await editor.getAction("editor.action.formatDocument")?.run();
459+
}
460+
await modelRef.object.save();
461+
});
394462

395463
editor.setModel(modelRef.object.textEditorModel);
396464

@@ -429,7 +497,7 @@ export default ({
429497
});
430498
};
431499
switchFile();
432-
}, [focused, editor, tabs]);
500+
}, [focused, editor, tabs, formatOnSave]);
433501

434502
return (
435503
<div className={"editor"}>

src/pages/IDE.tsx

Lines changed: 23 additions & 61 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, useRef, useState } from "react";
4+
import { useCallback, useContext, useEffect, useState } from "react";
55
import Editor from "../components/Tiles/Editor";
66
import MenuBar from "../components/Menu/MenuBar";
77
import "./IDE.css";
@@ -11,7 +11,6 @@ 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";
1514
import {
1615
Button,
1716
Divider,
@@ -26,7 +25,6 @@ import { restartServer } from "../utilities/lsp-client";
2625
import BottomBar from "../components/Tiles/BottomBar";
2726
import { open as openFileDialog } from "@tauri-apps/plugin-dialog";
2827
import { IStandaloneCodeEditor } from "@codingame/monaco-vscode-api/vscode/vs/editor/standalone/browser/standaloneCodeEditor";
29-
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
3028

3129
export interface IDEProps {}
3230

@@ -43,11 +41,11 @@ export default () => {
4341
const { storeInitialized, store } = useContext(StoreContext);
4442
const [openFile, setOpenFile] = useState<string | null>(null);
4543
const [openFiles, setOpenFiles] = useState<string[]>([]);
46-
const [saveFile, setSaveFile] = useState<(() => void) | null>(null);
44+
const [saveFile, setSaveFile] = useState<(() => Promise<void>) | null>(null);
4745
const [undo, setUndo] = useState<(() => void) | null>(null);
4846
const [redo, setRedo] = useState<(() => void) | null>(null);
4947
const [theme] = useStore<"light" | "dark">("appearance/theme", "dark");
50-
const { path: filePath } = useParams<"path">();
48+
const { path } = useParams<"path">();
5149
const { openFolderDialog, selectedToolchain, hasLimitedRam, initialized } =
5250
useIDE();
5351
const [sourcekitStartup, setSourcekitStartup] = useStore<boolean | null>(
@@ -59,84 +57,48 @@ export default () => {
5957
false
6058
);
6159

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

66-
const [callbacks, setCallbacks] = useState<Record<string, () => void>>({});
64+
const [callbacks, setCallbacks] = useState<
65+
Record<string, (() => void) | (() => Promise<void>)>
66+
>({});
6767
const navigate = useNavigate();
6868
const [projectValidation, setProjectValidation] =
6969
useState<ProjectValidation | null>(null);
7070
const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null);
7171
const { addToast } = useToast();
7272

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-
11173
useEffect(() => {
11274
(async () => {
113-
if (!store || !storeInitialized || !filePath) return;
114-
await store.set("last-opened-project", encodeURIComponent(filePath!));
75+
if (!store || !storeInitialized || !path) return;
76+
await store.set("last-opened-project", encodeURIComponent(path!));
11577
})();
116-
}, [filePath, store, storeInitialized]);
78+
}, [path, store, storeInitialized]);
11779

11880
useEffect(() => {
11981
if (
120-
filePath === undefined ||
121-
filePath === null ||
82+
path === undefined ||
83+
path === null ||
12284
selectedToolchain === null ||
12385
!initialized
12486
)
12587
return;
12688
setProjectValidation(null);
12789
(async () => {
128-
if (filePath) {
90+
if (path) {
12991
const toolchainPath = selectedToolchain?.path ?? "";
13092
const validation = await invoke<ProjectValidation>("validate_project", {
131-
projectPath: filePath,
93+
projectPath: path,
13294
toolchainPath: toolchainPath,
13395
});
13496
if (validation) {
13597
setProjectValidation(validation);
13698
}
13799
}
138100
})();
139-
}, [filePath, selectedToolchain, initialized]);
101+
}, [path, selectedToolchain, initialized]);
140102

141103
useEffect(() => {
142104
if (openFiles.length === 0) {
@@ -150,7 +112,7 @@ export default () => {
150112
useEffect(() => {
151113
let dispose = () => {};
152114

153-
if (filePath) {
115+
if (path) {
154116
const provider = new TauriFileSystemProvider(false);
155117
const overlayDisposable = registerFileSystemOverlay(1, provider);
156118
dispose = () => {
@@ -161,7 +123,7 @@ export default () => {
161123
return () => {
162124
dispose();
163125
};
164-
}, [filePath]);
126+
}, [path]);
165127

166128
useEffect(() => {
167129
let autoEnable = async () => {
@@ -176,17 +138,17 @@ export default () => {
176138
if (!sourcekitStartup || selectedToolchain == null) return;
177139
requestAnimationFrame(async () => {
178140
try {
179-
if (autoStartedLsp === filePath) return;
180-
autoStartedLsp = filePath;
181-
await restartServer(filePath, selectedToolchain);
141+
if (autoStartedLsp === path) return;
142+
autoStartedLsp = path;
143+
await restartServer(path, selectedToolchain);
182144
} catch (e) {
183145
console.error("Failed to start SourceKit-LSP:", e);
184146
addToast.error(
185147
"Failed to start SourceKit-LSP (see devtools for details). Some language features may not be available."
186148
);
187149
}
188150
});
189-
}, [sourcekitStartup, filePath, selectedToolchain]);
151+
}, [sourcekitStartup, path, selectedToolchain]);
190152

191153
const openNewFile = useCallback((file: string) => {
192154
setOpenFile(file);
@@ -205,7 +167,7 @@ export default () => {
205167

206168
useEffect(() => {
207169
setCallbacks({
208-
save: saveFile ?? (() => {}),
170+
save: saveFile ?? (async () => {}),
209171
openFolderDialog,
210172
newProject: () => navigate("/new"),
211173
welcomePage: () => navigate("/"),
@@ -224,7 +186,7 @@ export default () => {
224186
initialSizes={[20, 80]}
225187
>
226188
<Tile className="file-explorer-tile">
227-
<FileExplorer openFolder={filePath} setOpenFile={openNewFile} />
189+
<FileExplorer openFolder={path} setOpenFile={openNewFile} />
228190
</Tile>
229191
<Splitter
230192
gutterTheme={theme === "dark" ? GutterTheme.Dark : GutterTheme.Light}

src/preferences/pages/sourcekit.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@ import { createPreferencePage, createItems } from "../helpers";
22

33
export const sourceKitPage = createPreferencePage(
44
"sourcekit",
5-
"SourceKit LSP",
5+
"Language Features",
66
[
77
createItems.checkbox(
88
"startup",
99
"Auto-Launch SourceKit",
1010
"Automatically start sourcekit-lsp when you open a project",
1111
true
1212
),
13+
createItems.checkbox(
14+
"format",
15+
"Format on save",
16+
"Automatically format your code when you save",
17+
true
18+
),
1319
],
1420
{
15-
description: "Customize the look and feel of the application",
16-
category: "general",
21+
description: "Configure SourceKit-LSP and other language features",
22+
category: "swift",
1723
}
1824
);

src/preferences/pages/swift.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createCustomPreferencePage } from "../helpers";
55

66
export const swiftPage = createCustomPreferencePage(
77
"swift",
8-
"Swift",
8+
"Swift Setup",
99
() => (
1010
<div
1111
style={{

0 commit comments

Comments
 (0)