Skip to content

Commit 0c37b21

Browse files
committed
refactor(navigation): robust traversal logic and CI stability (#200)
- Fix getDateFromURI regression by using direct string prefix removal for base path stripping, avoiding collisions with timestamps. - Add explicit 'date' property to Input model to eliminate brittle relative-offset math in navigation commands. - Preserve active scope in Reader.loadEntryForInput. - Standardize default weekly file pattern to 'week_${week}.${ext}'. - Harden test mocks and integration suites for CI reliability. Closes #200
1 parent d486a9b commit 0c37b21

10 files changed

Lines changed: 68 additions & 35 deletions

File tree

src/actions/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export async function getAdjacentWeekInput(
211211
[direction === 'previous' ? 'subtract' : 'add'](1, 'week');
212212
const input = new Input();
213213
input.week = adj.week();
214+
input.scope = weekInfo.scope;
214215
return input;
215216
}
216217

src/actions/reader.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ export class Reader {
3939
public async loadEntryForInput(input: J.Model.Input): Promise<vscode.TextDocument> {
4040

4141
if (input.hasOffset()) {
42-
return this.loadEntryForDay(input.generateDate());
42+
return this.loadEntryForDay(input.generateDate(), input.scope);
4343
}
4444
if (input.hasWeek()) {
45-
return this.loadEntryForWeek(input.week);
45+
return this.loadEntryForWeek(input.week, input.scope);
4646
}
4747
throw Error("Neither offset nor week are defined in input, we abort.");
4848

@@ -53,12 +53,12 @@ export class Reader {
5353
* Loads the weekly page for the given week number (of the year)
5454
* @param week the week of the current year
5555
*/
56-
public async loadEntryForWeek(week: Number): Promise<vscode.TextDocument> {
56+
public async loadEntryForWeek(week: Number, scope?: string): Promise<vscode.TextDocument> {
5757
this.ctrl.logger.trace("Entering loadEntryForWeek() in actions/reader.ts for week " + week);
5858

5959
const [pathname, filename] = await Promise.all([
60-
this.ctrl.config.getWeekPathPattern(week),
61-
this.ctrl.config.getWeekFilePattern(week),
60+
this.ctrl.config.getWeekPathPattern(week, scope),
61+
this.ctrl.config.getWeekFilePattern(week, scope),
6262
]);
6363
const path = J.Util.resolvePath(pathname.value!, filename.value!);
6464

@@ -78,15 +78,15 @@ export class Reader {
7878
* @returns {Promise<vscode.TextDocument>} the document
7979
* @memberof Reader
8080
*/
81-
public async loadEntryForDay(date: Date): Promise<vscode.TextDocument> {
81+
public async loadEntryForDay(date: Date, scope?: string): Promise<vscode.TextDocument> {
8282
if (J.Util.isNullOrUndefined(date) || date!.toString().includes("Invalid")) {
8383
throw new Error("Invalid date");
8484
}
8585
this.ctrl.logger.trace("Entering loadEntryforDate() in actions/reader.ts for date " + date.toISOString());
8686

8787
const [pathname, filename] = await Promise.all([
88-
this.ctrl.config.getResolvedEntryPath(date),
89-
this.ctrl.config.getEntryFilePattern(date),
88+
this.ctrl.config.getResolvedEntryPath(date, scope),
89+
this.ctrl.config.getEntryFilePattern(date, scope),
9090
]);
9191
const path = J.Util.resolvePath(pathname.value!, filename.value!);
9292

src/ext/conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const defaultPatternDefinition: PatternDefinition =
6262
},
6363
weeks: {
6464
path: "${base}/${year}",
65-
file: "w${week}.${ext}"
65+
file: "week_${week}.${ext}"
6666
},
6767
weeklyNotes: {
6868
path: "${base}/${year}/w${week}",

src/model/input.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class Input {
3030
private _text: string = "";
3131
private _scope: string = "";
3232
private _week: number;
33+
private _date: Date | undefined;
3334

3435
private _tags: string[] = [];
3536

@@ -77,6 +78,13 @@ export class Input {
7778
return this._scope;
7879
}
7980

81+
/**
82+
* Getter date
83+
*/
84+
public get date(): Date | undefined {
85+
return this._date;
86+
}
87+
8088
/**
8189
* Setter offset
8290
* @param {number } value
@@ -113,6 +121,13 @@ export class Input {
113121
this._scope = value;
114122
}
115123

124+
/**
125+
* Setter date
126+
*/
127+
public set date(value: Date | undefined) {
128+
this._date = value;
129+
}
130+
116131
/**
117132
* Return the week of year
118133
*/
@@ -138,7 +153,7 @@ export class Input {
138153
}
139154

140155
public hasOffset(): boolean {
141-
return !isNaN(this.offset) && this._week === -1;
156+
return (this.date !== undefined) || (!isNaN(this.offset) && this._week === -1);
142157
}
143158

144159
public hasTask(): boolean {
@@ -151,6 +166,9 @@ export class Input {
151166
}
152167

153168
public generateDate(): Date {
169+
if (this.date) {
170+
return this.date;
171+
}
154172
let date = new Date();
155173
date.setDate(date.getDate() + this.offset);
156174
return date;

src/provider/commands/open-next-entry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export class OpenNextEntryCommand extends AbstractLoadEntryForDateCommand {
3737
return;
3838
}
3939
const input = new J.Model.Input();
40-
input.offset = daysBetween(new Date(), target);
40+
input.date = target;
41+
input.scope = anchor.scope;
4142
await this.execute(input);
4243
}
4344
}

src/provider/commands/open-previous-entry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export class OpenPreviousEntryCommand extends AbstractLoadEntryForDateCommand {
3737
return;
3838
}
3939
const input = new J.Model.Input();
40-
input.offset = daysBetween(new Date(), target);
40+
input.date = target;
41+
input.scope = anchor.scope;
4142
await this.execute(input);
4243
}
4344
}

src/test/suite/command-test-helpers.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ export function createMockCtrl(overrides: Partial<MockCtrl> = {}): MockCtrl {
3030
getTimeStringTemplate: async () => ({ value: '12:34' } as J.Model.ScopedTemplate),
3131
getTaskInlineTemplate: async () => ({ value: '- [ ] ${input}' } as J.Model.InlineTemplate),
3232
getBasePath: () => '/tmp/journal-tests',
33-
getBasePathForLocalOpen: () => '/tmp/journal-tests'
33+
getBasePathForLocalOpen: () => '/tmp/journal-tests',
34+
getFileExtension: () => 'md',
35+
getScopes: () => ['default'],
36+
getWeeksPathPatternRaw: () => '${base}/${year}',
37+
getWeeksFilePatternRaw: () => 'week_${week}.${ext}',
38+
getWeeklySyncConfig: () => ({ enabled: true, anchor: '## Daily Entries', template: '- [${weekday}](${link})', sortOrder: 'ascending' }),
39+
getResolvedEntryPath: async () => ({ value: '/tmp/journal-tests/2026/05' }),
40+
getEntryFilePattern: async () => ({ value: '17.md' }),
41+
getResolvedEntryPathForLocalOpen: async () => ({ value: '/tmp/journal-tests/2026/05' }),
42+
getWeekFilePattern: async () => ({ value: 'week_20.md' }),
43+
getWeekPathPatternForLocalOpen: async () => ({ value: '/tmp/journal-tests/2026' })
3444
},
3545
parser: {
3646
parseInput: async (input: string) => {

src/test/suite/commands-entry.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ suite('Command suites - entry commands', () => {
170170
getBasePathForLocalOpen: () => 'C:\\Users\\patrick.maue\\Git\\journal',
171171
getResolvedEntryPath: async (_date: Date) => ({ value: 'C:\\Users\\patrick.maue\\Git\\journal\\2026\\02' }),
172172
getEntryFilePattern: async (_date: Date) => ({ value: '2026-02-17.md' }),
173+
getResolvedEntryPathForLocalOpen: async (_date: Date) => ({ value: 'C:\\Users\\patrick.maue\\Git\\journal\\2026\\02' }),
173174
isWindowsStyleBaseConfigured: () => true
174175
},
175176
reader: {

src/test/suite/commands-prev-next.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ suite('Issue #144 — Open Previous / Open Next navigation', () => {
397397
async function seedWeeklyFile(base: string, year: number, week: number): Promise<string> {
398398
const yearDir = vscode.Uri.file(path.join(base, String(year).padStart(4, '0')));
399399
await vscode.workspace.fs.createDirectory(yearDir);
400-
const file = vscode.Uri.file(path.join(base, String(year).padStart(4, '0'), `w${week}.md`));
400+
const file = vscode.Uri.file(path.join(base, String(year).padStart(4, '0'), `week_${week}.md`));
401401
await vscode.workspace.fs.writeFile(file, new TextEncoder().encode(`# Week ${week}\n`));
402402
return file.fsPath;
403403
}

src/util/paths.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,35 +52,36 @@ export async function getDateFromURI(uri: string, pathTemplate: string, fileTemp
5252
if (fileTemplate.indexOf(".") > 0) { fileTemplate = fileTemplate.substring(0, fileTemplate.lastIndexOf(".")); }
5353
if (pathTemplate.startsWith("${base}/")) { pathTemplate = pathTemplate.substring("${base}/".length); }
5454

55-
// go through each element in path and assign it to a date part or skip it
56-
let pathParts = uri.split("/");
57-
58-
// check if part is in base path (if yes, we ignore)
59-
// for the rest: last part is file, everything else path pattern
60-
let pathElements: string[] = [];
61-
let trimmedPathString: string = "";
62-
let trimmedFileString = "";
63-
64-
pathParts.forEach((element, index) => {
65-
if (element.trim().length === 0) { return; }
66-
else if (element.startsWith("file:")) { return; }
67-
else if (basePath.search(element) >= 0) { return; }
68-
else if (index + 1 === pathParts.length) { trimmedFileString = element.substr(0, element.lastIndexOf(".")); }
69-
else {
70-
pathElements.concat(element);
71-
if (trimmedPathString.length > 1) { trimmedPathString += "/"; }
72-
trimmedPathString += element;
73-
}
74-
});
55+
// Normalize paths to use forward slashes and ensure absolute path for URI
56+
const normalizedUri = uri.replace(/\\/g, '/');
57+
const normalizedBase = basePath.replace(/\\/g, '/');
58+
59+
// Strip the base path prefix from the URI
60+
let relativePath = normalizedUri;
61+
if (normalizedUri.startsWith(normalizedBase)) {
62+
relativePath = normalizedUri.substring(normalizedBase.length).replace(/^\/+/, '');
63+
} else if (normalizedUri.includes('://')) {
64+
// Handle URI schemes (file:///...)
65+
try {
66+
const uriObj = vscode.Uri.parse(normalizedUri);
67+
const fsPath = uriObj.fsPath.replace(/\\/g, '/');
68+
if (fsPath.startsWith(normalizedBase)) {
69+
relativePath = fsPath.substring(normalizedBase.length).replace(/^\/+/, '');
70+
}
71+
} catch { /* fallback to path split */ }
72+
}
7573

74+
const pathParts = relativePath.split('/');
75+
const trimmedFileString = pathParts.length > 0 ? pathParts[pathParts.length - 1].split('.')[0] : "";
76+
const trimmedPathString = pathParts.length > 1 ? pathParts.slice(0, -1).join('/') : "";
7677

7778
const entryDateFormat = replaceDateTemplatesWithMomentsFormats(fileTemplate);
7879
const pathDateFormat = replaceDateTemplatesWithMomentsFormats(pathTemplate);
7980

8081
let parsedDateFromFile = moment(trimmedFileString, entryDateFormat);
8182
let parsedDateFromPath = moment(trimmedPathString, pathDateFormat);
8283

83-
let result = moment();
84+
let result = moment().startOf('day');
8485

8586
// consolidate the two
8687
if (fileTemplate.indexOf("${year}") >= 0) { result = result.year(parsedDateFromFile.year()); }

0 commit comments

Comments
 (0)