Skip to content

Commit

Permalink
fix(workspace): Dendron will try to parse non-dendron files in `onFir…
Browse files Browse the repository at this point in the history
…stOpen` (#2405)

* only perform frontmatter nav on dendron note

* update interfaces

* update tests

* update comments

* remove only

* fix tests

* remove comment

* update tests

* pr updates

* pr updates
  • Loading branch information
kevinslin committed Feb 15, 2022
1 parent 2a6c8bd commit d913a7f
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 10 deletions.
46 changes: 45 additions & 1 deletion packages/common-server/src/filesv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
NoteProps,
NotesCache,
NoteUtils,
RespV3,
SchemaModuleOpts,
SchemaModuleProps,
SchemaUtils,
Expand Down Expand Up @@ -516,4 +517,47 @@ export async function findNonNoteFile(opts: {
return undefined;
}

export { tmp, DirResult };
class FileUtils {
/**
* Check if a file starts with a prefix string
* @param fpath: full path to the file
* @param prefix: string prefix to check for
*/
static matchFilePrefix = async ({
fpath,
prefix,
}: {
fpath: string;
prefix: string;
}): Promise<RespV3<boolean>> => {
// solution adapted from https://stackoverflow.com/questions/70707646/reading-part-of-file-in-node
return new Promise((resolve) => {
const fileStream = fs.createReadStream(fpath, { highWaterMark: 60 });
const prefixLength = prefix.length;
fileStream
.on("error", (err) =>
resolve({
error: new DendronError({ innerError: err, message: "error" }),
})
)
// we got to the end without a match
.on("end", () => resolve({ data: false }))
.on("data", (chunk: Buffer) => {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < chunk.length; i++) {
const a = String.fromCharCode(chunk[i]);
// not a match, return
if (a !== prefix[i]) {
resolve({ data: false });
}
// all matches
if (i === prefixLength - 1) {
resolve({ data: true });
}
}
});
});
};
}

export { tmp, DirResult, FileUtils };
30 changes: 27 additions & 3 deletions packages/engine-server/src/workspace/utils.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import {
ConfigUtils,
CONSTANTS,
IntermediateDendronConfig,
DendronError,
DNodeUtils,
DVault,
DWorkspaceV2,
getSlugger,
IntermediateDendronConfig,
isBlockAnchor,
isNotUndefined,
NoteProps,
VaultUtils,
WorkspaceFolderCode,
WorkspaceOpts,
WorkspaceType,
ConfigUtils,
isNotUndefined,
} from "@dendronhq/common-all";
import {
FileUtils,
findDownTo,
findUpTo,
genHash,
Expand Down Expand Up @@ -102,6 +103,29 @@ export class WorkspaceUtils {
return dendronWorkspaceFolders.filter(isNotUndefined);
}

/**
* Check if a file is a dendron note (vs a regular file or something else entirely)
*/
static async isDendronNote({
wsRoot,
vaults,
fpath,
}: { fpath: string } & WorkspaceOpts): Promise<boolean> {
// check if we have markdown file
if (!fpath.endsWith(".md")) {
return false;
}
// if markdown file, check if it is in a dendron vault
if (!WorkspaceUtils.isPathInWorkspace({ wsRoot, vaults, fpath })) {
return false;
}

// if markdown file, does it have frontmatter? check for `---` at beginning of file
return (
(await FileUtils.matchFilePrefix({ fpath, prefix: "---" })).data || false
);
}

static isNativeWorkspace(workspace: DWorkspaceV2) {
return workspace.type === WorkspaceType.NATIVE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
goUpTo,
schemaModuleProps2File,
tmpDir,
FileUtils,
} from "@dendronhq/common-server";
import { FileTestUtils } from "@dendronhq/common-test-utils";
import fs from "fs-extra";
import _ from "lodash";
import path from "path";
Expand Down Expand Up @@ -158,3 +160,66 @@ describe("file2Schema", () => {
expect(_.values(schema.schemas).length).toEqual(8);
});
});

describe("GIVEN matchFilePrefix", () => {
let fpath: string;
const prefix = "---";

beforeEach(() => {
const root = FileTestUtils.tmpDir().name;
fpath = path.join(root, "test-file.md");
});

describe("WHEN file starts with prefix", () => {
test("THEN return true", async () => {
fs.writeFileSync(fpath, "---\nfoo");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: true,
});
});
});

describe("WHEN file matches prefix exactly", () => {
test("THEN return true", async () => {
fs.writeFileSync(fpath, "---");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: true,
});
});
});

describe("WHEN file does not start with prefix", () => {
test("THEN return false", async () => {
fs.writeFileSync(fpath, "--!");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: false,
});
});
});

describe("WHEN file is empty", () => {
test("THEN return false", async () => {
fs.writeFileSync(fpath, "");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: false,
});
});
});

describe("WHEN file is shorter than prefix", () => {
test("THEN return false", async () => {
fs.writeFileSync(fpath, "--");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: false,
});
});
});
describe("WHEN file match is not at start of file", () => {
test("THEN return false", async () => {
fs.writeFileSync(fpath, " ---");
expect(await FileUtils.matchFilePrefix({ fpath, prefix })).toEqual({
data: false,
});
});
});
});
5 changes: 3 additions & 2 deletions packages/plugin-core/src/WSUtilsV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import { WorkspaceUtils } from "@dendronhq/engine-server";
let WS_UTILS: IWSUtilsV2 | undefined;

/**
* Non static WSUtils to allow unwinding of our circular dependencies.
* */
*
* Utilities to work with workspace related functions
**/
export class WSUtilsV2 implements IWSUtilsV2 {
private extension: IDendronExtension;

Expand Down
36 changes: 36 additions & 0 deletions packages/plugin-core/src/WorkspaceWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export class WorkspaceWatcher {
this._windowWatcher = windowWatcher;
}

// eslint-disable-next-line camelcase
__DO_NOT_USE_IN_PROD_exposePropsForTesting() {
return {
onFirstOpen: _.bind(this.onFirstOpen, this),
};
}

activate(context: ExtensionContext) {
this._extension.addDisposable(
workspace.onWillSaveTextDocument(
Expand Down Expand Up @@ -233,6 +240,11 @@ export class WorkspaceWatcher {
}
}

/**
* If note is in workspace, execute {@link onWillSaveNote}
* @param event
* @returns
*/
async onWillSaveTextDocument(
event: TextDocumentWillSaveEvent
): Promise<{ changes: TextEdit[] }> {
Expand Down Expand Up @@ -273,6 +285,13 @@ export class WorkspaceWatcher {
}
}

/**
* When saving a note, do some book keeping
* - update the `updated` time in frontmatter
* - update the note metadata in the engine
* @param event
* @returns
*/
private onWillSaveNote(event: TextDocumentWillSaveEvent) {
const ctx = "WorkspaceWatcher:onWillSaveNote";
const uri = event.document.uri;
Expand All @@ -295,13 +314,16 @@ export class WorkspaceWatcher {
const match = NoteUtils.RE_FM_UPDATED.exec(content);
let changes: TextEdit[] = [];

// update the `updated` time in frontmatter
if (match && parseInt(match[1], 10) !== note.updated) {
Logger.info({ ctx, match, msg: "update activeText editor" });
const startPos = event.document.positionAt(match.index);
const endPos = event.document.positionAt(match.index + match[0].length);
changes = [
TextEdit.replace(new Range(startPos, endPos), `updated: ${now}`),
];

// update the note in engine
// eslint-disable-next-line no-async-promise-executor
const p = new Promise(async (resolve) => {
note.updated = now;
Expand Down Expand Up @@ -423,12 +445,25 @@ export class WorkspaceWatcher {
}
}

/**
* Dendron will perform changes like moving the cursor when first opening a Dendron note
* @returns boolean : returns `true` if Dendron made changes during `onFirstOpen` and `false` otherwise
*/
private async onFirstOpen(editor: TextEditor) {
Logger.info({
ctx: context("onFirstOpen"),
msg: "enter",
fname: NoteUtils.uri2Fname(editor.document.uri),
});
const { vaults, wsRoot } = this._extension.getDWorkspace();
const fpath = editor.document.uri.fsPath;

// don't apply actions to non-dendron notes
// NOTE: in the future if we add `onFirstOpen` actions to non-dendron notes, this logic will need to be updated
if (!(await WorkspaceUtils.isDendronNote({ wsRoot, vaults, fpath }))) {
return false;
}

WorkspaceWatcher.moveCursorPastFrontmatter(editor);
const config = this._extension.getDWorkspace().config;
if (ConfigUtils.getWorkspace(config).enableAutoFoldFrontmatter) {
Expand All @@ -439,6 +474,7 @@ export class WorkspaceWatcher {
msg: "exit",
fname: NoteUtils.uri2Fname(editor.document.uri),
});
return true;
}

static moveCursorPastFrontmatter(editor: TextEditor) {
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-core/src/dendronExtensionInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FileWatcher } from "./fileWatcher";
import { IEngineAPIService } from "./services/EngineAPIServiceInterface";
import { INoteSyncService } from "./services/NoteSyncService";
import { ISchemaSyncService } from "./services/SchemaSyncServiceInterface";
import { WorkspaceWatcher } from "./WorkspaceWatcher";
import { IWSUtilsV2 } from "./WSUtilsV2Interface";

export type DendronWorkspaceSettings = Partial<{
Expand Down Expand Up @@ -57,6 +58,7 @@ export type DendronWorkspaceSettings = Partial<{
export interface IDendronExtension {
port?: number;
context: vscode.ExtensionContext;
workspaceWatcher?: WorkspaceWatcher;
serverWatcher?: vscode.FileSystemWatcher;
fileWatcher?: FileWatcher;
type: WorkspaceType;
Expand All @@ -70,6 +72,11 @@ export interface IDendronExtension {
noteLookupProviderFactory: INoteLookupProviderFactory;
schemaLookupProviderFactory: ISchemaLookupProviderFactory;

activateWatchers(): Promise<void>;
/**
* This will deactivate the entire Dendron Extension. Takes care of disposing of all resources that Dendron has created
*/
deactivate(): Promise<void>;
pauseWatchers<T = void>(cb: () => Promise<T>): Promise<T>;

getClientAPIRootUrl(): Promise<string>;
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-core/src/test/MockDendronExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export class MockDendronExtension implements IDendronExtension {
throw new Error("Method not implemented in MockDendronExtension.");
}

async activateWatchers(): Promise<void> {
return;
}

async deactivate(): Promise<void> {
return;
}

/**
* Note: No-Op
* @param _cb
Expand Down

0 comments on commit d913a7f

Please sign in to comment.