Skip to content

Commit b2d0413

Browse files
pajomaclaude
andcommitted
refactor(di): replace Ctrl service-locator with app/Container
Phase 2 of #234. Introduce src/app/ composition root: Container (implements JournalController, single-pass ctor with a logger factory — removes two-phase initServices/!), register.ts (command/provider wiring), startup.ts (lifecycle, moved from vscode/). Consumers now depend on the JournalController interface, not the concrete controller class. Expand IConfiguration with the methods consumers actually call (getNavigationMode, getScopeDefinitions, getWeek*PatternRaw, *ForLocalOpen, getWeeklySyncConfig, getTimeStringTemplate, get*LinkInlineTemplate). Move WeeklySyncConfig type to model. paths.ts now types config as IConfiguration. Delete src/util/controller.ts and src/vscode/startup.ts. Refs #234. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c699f77 commit b2d0413

52 files changed

Lines changed: 461 additions & 525 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/container.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (C) 2018 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+
19+
'use strict';
20+
21+
import * as vscode from 'vscode';
22+
import { IConfiguration, IFileSystem, ILogger, IWorkspaceConfigReader, JournalController } from '../model';
23+
import { Configuration } from '../vscode/conf';
24+
import { Parser } from '../journal/parser';
25+
import { Writer } from '../journal/writer';
26+
import { Reader } from '../journal/reader';
27+
import { Inject } from '../journal/inject';
28+
import { Dialogues } from '../vscode/dialogues';
29+
import { VscodeFileSystem } from '../vscode/vscode-fs';
30+
31+
/**
32+
* Builds the logger once the configuration is available. The logger needs the
33+
* configuration (for trace/dev settings), so the container resolves the config
34+
* first and then hands it to this factory — single-pass, no two-phase init.
35+
*/
36+
export type LoggerFactory = (config: IConfiguration) => ILogger;
37+
38+
/**
39+
* Composition root. Constructs the full service graph in one pass and exposes
40+
* it through the {@link JournalController} interface. Consumers depend on the
41+
* interface, never on this concrete class — only the registration layer
42+
* (`app/`) instantiates it.
43+
*/
44+
export class Container implements JournalController {
45+
46+
public readonly config: Configuration;
47+
public readonly logger: ILogger;
48+
public readonly fs: IFileSystem;
49+
public readonly inject: Inject;
50+
public readonly parser: Parser;
51+
public readonly ui: Dialogues;
52+
public readonly writer: Writer;
53+
public readonly reader: Reader;
54+
55+
constructor(configSource: IWorkspaceConfigReader, loggerFactory: LoggerFactory) {
56+
this.config = new Configuration(configSource);
57+
this.logger = loggerFactory(this.config);
58+
this.fs = new VscodeFileSystem();
59+
this.inject = new Inject(this.config, this.logger);
60+
this.parser = new Parser(this.config, this.logger);
61+
this.ui = new Dialogues(this.config, this.logger, this.parser, this.fs);
62+
this.writer = new Writer(this.config, this.logger, this.inject, this.fs,
63+
async (path) => vscode.workspace.openTextDocument(vscode.Uri.file(path)));
64+
this.reader = new Reader(this.config, this.logger, this.writer, this.ui, this.fs);
65+
}
66+
}

src/app/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (C) 2018 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+
19+
export { Container, LoggerFactory } from './container';
20+
export { Startup } from './startup';
21+
export { registerCommands, registerCodeActions, registerCacheInvalidation } from './register';

src/app/register.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (C) 2018 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+
19+
'use strict';
20+
21+
import * as vscode from 'vscode';
22+
import { Container } from './container';
23+
import {
24+
OpenJournalWorkspaceCommand, OpenNextEntryCommand, OpenPreviousEntryCommand,
25+
PrintDurationCommand, PrintSumCommand, PrintTimeCommand, ShiftTaskCommand,
26+
ShowEntryForInputCommand, ShowEntryForTodayCommand, ShowEntryForTomorrowCommand,
27+
ShowEntryForYesterdayCommand, ShowNoteCommand,
28+
} from '../commands';
29+
import { SyncDailyLinks, SyncNoteLinks, WeeklyEntryWatcher } from '../features';
30+
import { CompletedTaskActions, OpenTaskActions } from '../ui';
31+
32+
const MARKDOWN_SELECTOR: vscode.DocumentSelector = { scheme: 'file', language: 'markdown' };
33+
34+
/** Registers all journal commands and the weekly/note sync wiring. */
35+
export function registerCommands(ctrl: Container, context: vscode.ExtensionContext): void {
36+
ctrl.logger.trace("Entering registerCommands() in app/register.ts");
37+
38+
ctrl.reader.onNotesInjected = (doc, date) => {
39+
new SyncNoteLinks(ctrl).injectAttachmentLinks(doc, date)
40+
.finally(() => ctrl.logger.trace("Scanning notes completed"));
41+
};
42+
43+
const syncDailyLinks = new SyncDailyLinks(ctrl);
44+
context.subscriptions.push(new WeeklyEntryWatcher(ctrl, syncDailyLinks));
45+
46+
context.subscriptions.push(
47+
OpenJournalWorkspaceCommand.create(ctrl),
48+
PrintTimeCommand.create(ctrl),
49+
PrintSumCommand.create(ctrl),
50+
PrintDurationCommand.create(ctrl),
51+
ShowEntryForInputCommand.create(ctrl),
52+
vscode.commands.registerCommand('journal.memo', () =>
53+
new ShowEntryForInputCommand(ctrl).execute()),
54+
ShowEntryForTodayCommand.create(ctrl),
55+
ShowEntryForTomorrowCommand.create(ctrl),
56+
ShowEntryForYesterdayCommand.create(ctrl),
57+
ShowNoteCommand.create(ctrl),
58+
ShiftTaskCommand.create(ctrl),
59+
OpenPreviousEntryCommand.create(ctrl),
60+
OpenNextEntryCommand.create(ctrl),
61+
);
62+
}
63+
64+
/** Registers the markdown code-action providers (task state transitions). */
65+
export function registerCodeActions(ctrl: Container, context: vscode.ExtensionContext): void {
66+
// TODO: add filters only for configured base directories
67+
context.subscriptions.push(
68+
vscode.languages.registerCodeActionsProvider(MARKDOWN_SELECTOR, new CompletedTaskActions(ctrl)),
69+
vscode.languages.registerCodeActionsProvider(MARKDOWN_SELECTOR, new OpenTaskActions(ctrl)),
70+
);
71+
}
72+
73+
/** Wires the entry-scan cache invalidation listeners. */
74+
export function registerCacheInvalidation(ctrl: Container, context: vscode.ExtensionContext): void {
75+
try {
76+
const scanner = ctrl.ui.getScanner();
77+
context.subscriptions.push(...scanner.registerInvalidationListeners());
78+
} catch (error) {
79+
ctrl.logger.error("Failed to register cache invalidation listeners, reason: ", error);
80+
}
81+
}

src/app/startup.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (C) 2018 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+
19+
'use strict';
20+
21+
import * as vscode from 'vscode';
22+
import * as Path from 'path';
23+
import { isNullOrUndefined, ConsoleLogger } from '../util';
24+
import { Configuration } from '../vscode/conf';
25+
import { Container } from './container';
26+
import { registerCacheInvalidation, registerCodeActions, registerCommands } from './register';
27+
28+
interface TextMateRule { scope: string; settings: any; }
29+
30+
/**
31+
* Extension lifecycle entry point. Builds the {@link Container} (composition
32+
* root) in a single pass, then registers commands, providers and optional
33+
* syntax highlighting.
34+
*/
35+
export class Startup {
36+
37+
private ctrl!: Container;
38+
39+
constructor(public config: vscode.WorkspaceConfiguration) { }
40+
41+
public async run(context: vscode.ExtensionContext): Promise<void> {
42+
try {
43+
const channel = vscode.window.createOutputChannel("Journal");
44+
context.subscriptions.push(channel);
45+
46+
this.ctrl = new Container(this.config, (cfg) => new ConsoleLogger(cfg, channel));
47+
this.ctrl.logger.debug("VSCode Journal is starting");
48+
49+
if (this.ctrl.config.isDevelopmentModeEnabled()) {
50+
console.log("Development Mode for Journal extension is enabled, Tracing in Console and Output is activated.");
51+
}
52+
53+
registerCommands(this.ctrl, context);
54+
registerCodeActions(this.ctrl, context);
55+
await this.registerSyntaxHighlighting(this.ctrl);
56+
registerCacheInvalidation(this.ctrl, context);
57+
58+
console.timeEnd("startup");
59+
console.log("VSCode-Journal extension was successfully initialized.");
60+
} catch (error) {
61+
console.error(error);
62+
throw error;
63+
}
64+
}
65+
66+
public getConfiguration(): Configuration {
67+
return this.ctrl.config;
68+
}
69+
70+
public getJournalController(): Container {
71+
return this.ctrl;
72+
}
73+
74+
public async registerSyntaxHighlighting(ctrl: Container): Promise<Container> {
75+
if (this.ctrl.config.isSyntaxHighlightingEnabled()) {
76+
return this.enableSyntaxHighlighting(ctrl);
77+
} else {
78+
return this.disableSyntaxHighlighting(ctrl);
79+
}
80+
}
81+
82+
public async disableSyntaxHighlighting(ctrl: Container): Promise<Container> {
83+
const tokenColorCustomizations = vscode.workspace.getConfiguration('editor.tokenColorCustomizations');
84+
if (!tokenColorCustomizations.has("textMateRules")) { return ctrl; }
85+
86+
const rules: TextMateRule[] = tokenColorCustomizations.get<TextMateRule[]>("textMateRules")!;
87+
const result: TextMateRule[] = rules.filter(rule => !rule.scope.includes("journal"));
88+
await vscode.workspace.getConfiguration().update("editor.tokenColorCustomizations", { "textMateRules": result }, vscode.ConfigurationTarget.Global);
89+
return ctrl;
90+
}
91+
92+
/**
93+
* Sets default syntax highlighting settings on startup, we try to differentiate between dark and light themes
94+
*/
95+
public async enableSyntaxHighlighting(ctrl: Container): Promise<Container> {
96+
const theme: string | undefined = vscode.workspace.getConfiguration().get<string>("workbench.colorTheme");
97+
let style: string;
98+
if (isNullOrUndefined(theme) || theme!.search('Light') > -1) { style = "light"; }
99+
else if (theme!.search('High Contrast') > -1) { style = "high-contrast"; }
100+
else { style = "dark"; }
101+
102+
const tokenColorCustomizations: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('editor.tokenColorCustomizations');
103+
const rules: TextMateRule[] | undefined = tokenColorCustomizations.get<TextMateRule[]>("textMateRules");
104+
105+
if (isNullOrUndefined(rules) || rules!.length > 0) {
106+
return ctrl;
107+
}
108+
109+
if (style.startsWith("high-contrast")) { return ctrl; }
110+
111+
const ext: vscode.Extension<any> | undefined = vscode.extensions.getExtension("pajoma.vscode-journal");
112+
if (isNullOrUndefined(ext)) { throw Error("Failed to load this extension"); }
113+
114+
const colorConfigDir: string = Path.join(ext!.extensionPath, "res", "colors");
115+
const rawData = await vscode.workspace.fs.readFile(vscode.Uri.file(Path.join(colorConfigDir, style + ".json")));
116+
const data = Buffer.from(rawData).toString('utf-8');
117+
118+
// FIXME: this is a workaround, since we can't simply inject the textMateRules here (not registered configuration)
119+
const existingConfig = vscode.workspace.getConfiguration('editor').get('tokenColorCustomizations');
120+
const mutableExistingConfig = JSON.parse(JSON.stringify(existingConfig));
121+
mutableExistingConfig.textMateRules = JSON.parse(data);
122+
await vscode.workspace.getConfiguration("editor").update("tokenColorCustomizations", mutableExistingConfig, vscode.ConfigurationTarget.Global);
123+
124+
return ctrl;
125+
}
126+
}

src/commands/copy-task.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
import moment = require('moment');
2121
import * as vscode from 'vscode';
22-
import { InlineString, InlineTemplate } from '../model';
23-
import { Ctrl } from '../util';
22+
import { JournalController, InlineString, InlineTemplate } from '../model';
2423

2524
export enum ShiftTarget {
2625
nextWorkingDay,
@@ -43,13 +42,13 @@ export class CopyTaskCommand implements vscode.Command {
4342
command: string = "journal.commands.copy-task";
4443

4544

46-
protected constructor(public ctrl: Ctrl) { }
45+
protected constructor(public ctrl: JournalController) { }
4746

4847
public async dispose(): Promise<void> {
4948
// do nothing
5049
}
5150

52-
public static create(ctrl: Ctrl): vscode.Disposable {
51+
public static create(ctrl: JournalController): vscode.Disposable {
5352
const cmd = new this(ctrl);
5453
vscode.commands.registerCommand(cmd.command, (document, range, target) => cmd.execute(document, range, target));
5554
return cmd;

src/commands/open-journal-workspace.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'use strict';
1919

2020
import * as vscode from 'vscode';
21-
import { Ctrl } from '../util';
21+
import { JournalController } from '../model';
2222
import { isRemoteSession, toLocalFileUri } from '../journal/paths';
2323

2424

@@ -27,13 +27,13 @@ export class OpenJournalWorkspaceCommand implements vscode.Command, vscode.Dispo
2727
command: string = 'journal.open';
2828

2929

30-
protected constructor(public ctrl: Ctrl) { }
30+
protected constructor(public ctrl: JournalController) { }
3131

3232
public async dispose(): Promise<void> {
3333
// do nothing
3434
}
3535

36-
public static create(ctrl: Ctrl): vscode.Disposable {
36+
public static create(ctrl: JournalController): vscode.Disposable {
3737
const cmd = new this(ctrl);
3838
vscode.commands.registerCommand(cmd.command, () => cmd.openWorkspace());
3939
return cmd;
@@ -92,4 +92,4 @@ export class OpenJournalWorkspaceCommand implements vscode.Command, vscode.Dispo
9292
}
9393

9494

95-
}
95+
}

src/commands/open-next-entry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
'use strict';
1111

1212
import * as vscode from 'vscode';
13-
import { Input } from '../model';
14-
import { Ctrl } from '../util';
13+
import { JournalController, Input } from '../model';
1514
import { AbstractLoadEntryForDateCommand } from './show-entry-for-date';
1615
import { daysBetween, findAdjacentEntry, getAdjacentWeekInput, resolveAnchor, Mode } from '../journal/navigation';
1716

1817
export class OpenNextEntryCommand extends AbstractLoadEntryForDateCommand {
1918
title: string = "Open the next journal entry";
2019
command: string = "journal.openNext";
2120

22-
public static create(ctrl: Ctrl): vscode.Disposable {
21+
public static create(ctrl: JournalController): vscode.Disposable {
2322
const cmd = new this(ctrl);
2423
vscode.commands.registerCommand(cmd.command, () => cmd.run());
2524
return cmd;

src/commands/open-previous-entry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
'use strict';
1111

1212
import * as vscode from 'vscode';
13-
import { Input } from '../model';
14-
import { Ctrl } from '../util';
13+
import { JournalController, Input } from '../model';
1514
import { AbstractLoadEntryForDateCommand } from './show-entry-for-date';
1615
import { daysBetween, findAdjacentEntry, getAdjacentWeekInput, resolveAnchor, Mode } from '../journal/navigation';
1716

1817
export class OpenPreviousEntryCommand extends AbstractLoadEntryForDateCommand {
1918
title: string = "Open the previous journal entry";
2019
command: string = "journal.openPrevious";
2120

22-
public static create(ctrl: Ctrl): vscode.Disposable {
21+
public static create(ctrl: JournalController): vscode.Disposable {
2322
const cmd = new this(ctrl);
2423
vscode.commands.registerCommand(cmd.command, () => cmd.run());
2524
return cmd;

0 commit comments

Comments
 (0)