Skip to content

Commit eba2a5c

Browse files
pajomaclaude
andcommitted
fix(remote): stat-first file creation, removes error toast on first use (#51)
On Remote SSH and WSL, Reader.loadEntryForDay, Reader.loadEntryForWeek, and LoadNotes.loadNote called vscode.workspace.openTextDocument on a missing file and relied on catching the rejection. VS Code surfaces an error notification independently of the catch, and the catch itself filtered errors by prefix-matching the message text — fragile and inconsistent across the three call sites (the week handler only matched "cannot open file:", leaving remote weeks broken). Replace with stat-first via a dedicated fileExists(uri) helper that wraps vscode.workspace.fs.stat and converts FileSystemError.FileNotFound to false. Anything else (permission denied, transport unavailable) propagates so VS Code can surface its own dialog. While each method is rewritten, drop the new Promise((resolve, reject) => ...) wrapper and use native async/await. Scope limited to the three touched methods; PLAN.md Phase 2.2 still owns the rest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fd9968e commit eba2a5c

4 files changed

Lines changed: 88 additions & 94 deletions

File tree

src/actions/reader.ts

Lines changed: 44 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -52,96 +52,64 @@ export class Reader {
5252
* @param week the week of the current year
5353
*/
5454
public async loadEntryForWeek(week: Number): Promise<vscode.TextDocument> {
55-
return new Promise<vscode.TextDocument>((resolve, reject) => {
56-
this.ctrl.logger.trace("Entering loadEntryForWeek() in actions/reader.ts for week " + week);
57-
58-
let path: string = "";
59-
60-
Promise.all([
61-
this.ctrl.config.getWeekPathPattern(week),
62-
this.ctrl.config.getWeekFilePattern(week)
63-
64-
]).then(([pathname, filename]) => {
65-
path = J.Util.resolvePath(pathname.value!, filename.value!);
66-
return this.ctrl.ui.openDocument(path);
67-
68-
}).catch((reason: any) => {
69-
if (reason instanceof Error) {
70-
if (!reason.message.startsWith("cannot open file:")) {
71-
this.ctrl.logger.printError(reason);
72-
reject(reason);
73-
}
74-
}
75-
return this.ctrl.writer.createWeeklyForPath(path, week);
76-
77-
}).then((_doc: vscode.TextDocument) => {
78-
this.ctrl.logger.debug("loadEntryForWeek() - Loaded file in:", _doc.uri.toString());
79-
resolve(_doc);
80-
81-
}).catch((error: Error) => {
82-
this.ctrl.logger.printError(error);
83-
reject("Failed to load entry for week: " + week);
84-
});
85-
});
55+
this.ctrl.logger.trace("Entering loadEntryForWeek() in actions/reader.ts for week " + week);
56+
57+
const [pathname, filename] = await Promise.all([
58+
this.ctrl.config.getWeekPathPattern(week),
59+
this.ctrl.config.getWeekFilePattern(week),
60+
]);
61+
const path = J.Util.resolvePath(pathname.value!, filename.value!);
62+
63+
const doc = await this.openOrCreate(
64+
path,
65+
() => this.ctrl.writer.createWeeklyForPath(path, week),
66+
);
67+
this.ctrl.logger.debug("loadEntryForWeek() - Loaded file in:", doc.uri.toString());
68+
return doc;
8669
}
8770

8871

8972
/**
90-
* Loads the journal entry for the given date. If no entry exists, promise is rejected with the invalid path
73+
* Loads the journal entry for the given date. If no entry exists, it is created.
9174
*
9275
* @param {Date} date the date for the entry
93-
* @returns {Q.Promise<vscode.TextDocument>} the document
94-
* @throws {string} error message
76+
* @returns {Promise<vscode.TextDocument>} the document
9577
* @memberof Reader
9678
*/
97-
public async loadEntryForDay(date: Date): Promise<vscode.TextDocument> {
98-
99-
return new Promise<vscode.TextDocument>((resolve, reject) => {
100-
if (J.Util.isNullOrUndefined(date) || date!.toString().includes("Invalid")) {
101-
reject("Invalid date");
102-
return;
103-
}
104-
105-
this.ctrl.logger.trace("Entering loadEntryforDate() in actions/reader.ts for date " + date.toISOString());
106-
107-
let path: string = "";
108-
109-
Promise.all([
110-
this.ctrl.config.getResolvedEntryPath(date),
111-
this.ctrl.config.getEntryFilePattern(date)
112-
113-
]).then(([pathname, filename]) => {
114-
path = J.Util.resolvePath(pathname.value!, filename.value!);
115-
return this.ctrl.ui.openDocument(path);
116-
117-
118-
}).catch((reason: any) => {
119-
if (reason instanceof Error) {
120-
if (!reason.message.startsWith("cannot open file:") && !reason.message.startsWith("cannot open vscode-remote:")) {
121-
this.ctrl.logger.printError(reason);
122-
reject(reason);
123-
}
124-
}
125-
return this.ctrl.writer.createEntryForPath(path, date);
79+
public async loadEntryForDay(date: Date): Promise<vscode.TextDocument> {
80+
if (J.Util.isNullOrUndefined(date) || date!.toString().includes("Invalid")) {
81+
throw new Error("Invalid date");
82+
}
83+
this.ctrl.logger.trace("Entering loadEntryforDate() in actions/reader.ts for date " + date.toISOString());
12684

127-
}).then((_doc: vscode.TextDocument) => {
128-
this.ctrl.logger.debug("loadEntryForDate() - Loaded file in:", _doc.uri.toString());
129-
new J.Provider.SyncNoteLinks(this.ctrl).injectAttachementLinks(_doc, date)
130-
.finally(() =>
131-
// do nothing
132-
this.ctrl.logger.trace("Scanning notes completed")
133-
);
134-
resolve(_doc);
85+
const [pathname, filename] = await Promise.all([
86+
this.ctrl.config.getResolvedEntryPath(date),
87+
this.ctrl.config.getEntryFilePattern(date),
88+
]);
89+
const path = J.Util.resolvePath(pathname.value!, filename.value!);
13590

136-
}).catch((error: Error) => {
137-
this.ctrl.logger.printError(error);
138-
reject("Failed to load entry for date: " + date.toDateString());
91+
const doc = await this.openOrCreate(
92+
path,
93+
() => this.ctrl.writer.createEntryForPath(path, date),
94+
);
95+
this.ctrl.logger.debug("loadEntryForDate() - Loaded file in:", doc.uri.toString());
13996

140-
});
97+
new J.Provider.SyncNoteLinks(this.ctrl).injectAttachementLinks(doc, date)
98+
.finally(() => this.ctrl.logger.trace("Scanning notes completed"));
14199

142-
});
100+
return doc;
143101
}
144102

103+
private async openOrCreate(
104+
path: string,
105+
create: () => Promise<vscode.TextDocument>,
106+
): Promise<vscode.TextDocument> {
107+
const exists = await J.Util.fileExists(vscode.Uri.file(path));
108+
if (exists) {
109+
return this.ctrl.ui.openDocument(path);
110+
}
111+
return create();
112+
}
145113
}
146114

147115

src/provider/features/load-note.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,21 @@ export class LoadNotes {
3232
}
3333

3434
/**
35-
* Creates or loads a note
35+
* Creates or loads a note
3636
*
3737
* @param {string} path
3838
* @param {string} content
3939
* @returns {Promise<vscode.TextDocument>}
4040
* @memberof Writer
4141
*/
42-
public async loadNote(path: string, content: string): Promise<vscode.TextDocument> {
42+
public async loadNote(path: string, content: string): Promise<vscode.TextDocument> {
4343
this.ctrl.logger.trace("Entering loadNote() in features/load-note.ts for path: ", path);
4444

45-
return new Promise<vscode.TextDocument>((resolve, reject) => {
46-
// check if file exists already
47-
48-
this.ctrl.ui.openDocument(path)
49-
.then((doc: vscode.TextDocument) => resolve(doc))
50-
.catch(error => {
51-
this.ctrl.writer.createSaveLoadTextDocument(path, content)
52-
.then((doc: vscode.TextDocument) => resolve(doc))
53-
.catch(error => {
54-
this.ctrl.logger.error(error);
55-
reject("Failed to load note.");
56-
});
57-
});
58-
59-
});
45+
const exists = await J.Util.fileExists(vscode.Uri.file(path));
46+
if (exists) {
47+
return this.ctrl.ui.openDocument(path);
48+
}
49+
return this.ctrl.writer.createSaveLoadTextDocument(path, content);
6050
}
6151

6252

src/util/fs-exists.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (C) 2026 Patrick Maué
2+
//
3+
// This file is part of vscode-journal.
4+
//
5+
// vscode-journal is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// vscode-journal is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with vscode-journal. If not, see <http://www.gnu.org/licenses/>.
17+
18+
'use strict';
19+
20+
import * as vscode from 'vscode';
21+
22+
// Returns true if the URI resolves to an existing entry on the workspace
23+
// filesystem, false if it definitely does not exist, and rethrows for any
24+
// other FS error so callers (and VS Code) can surface a notification.
25+
export async function fileExists(uri: vscode.Uri): Promise<boolean> {
26+
try {
27+
await vscode.workspace.fs.stat(uri);
28+
return true;
29+
} catch (err) {
30+
if (err instanceof vscode.FileSystemError && err.code === 'FileNotFound') {
31+
return false;
32+
}
33+
throw err;
34+
}
35+
}

src/util/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ export {
5151
getPathOfMonth,
5252
inferType,
5353
resolvePath,
54-
} from './paths';
54+
} from './paths';
55+
export { fileExists } from './fs-exists';

0 commit comments

Comments
 (0)