Skip to content

Commit

Permalink
Merge branch 'main' into pathe
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreasArvidsson committed Jul 11, 2024
2 parents e0ee609 + 00651ad commit 850c1f9
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 261 deletions.
2 changes: 1 addition & 1 deletion packages/cursorless-engine/src/actions/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class Actions implements ActionRecord {
foldRegion = new Fold(this.rangeUpdater);
followLink = new FollowLink({ openAside: false });
followLinkAside = new FollowLink({ openAside: true });
generateSnippet = new GenerateSnippet();
generateSnippet = new GenerateSnippet(this.snippets);
getText = new GetText();
highlight = new Highlight();
increment = new Increment(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FlashStyle, isTesting, Range } from "@cursorless/common";
import type { Snippets } from "../../core/Snippets";
import { Offsets } from "../../processTargets/modifiers/surroundingPair/types";
import { ide } from "../../singletons/ide.singleton";
import { Target } from "../../typings/target.types";
import type { Target } from "../../typings/target.types";
import { matchAll } from "../../util/regex";
import { ensureSingleTarget, flashTargets } from "../../util/targetUtils";
import { ActionReturnValue } from "../actions.types";
import type { ActionReturnValue } from "../actions.types";
import { constructSnippetBody } from "./constructSnippetBody";
import { editText } from "./editText";
import { openNewSnippetFile } from "./openNewSnippetFile";
import Substituter from "./Substituter";

/**
Expand Down Expand Up @@ -46,7 +46,7 @@ import Substituter from "./Substituter";
* confusing escaping.
*/
export default class GenerateSnippet {
constructor() {
constructor(private snippets: Snippets) {
this.run = this.run.bind(this);
}

Expand Down Expand Up @@ -228,7 +228,7 @@ export default class GenerateSnippet {
} else {
// Otherwise, we create and open a new document for the snippet in the
// user snippets dir
await openNewSnippetFile(snippetName);
await this.snippets.openNewSnippetFile(snippetName);
}

// Insert the meta-snippet
Expand Down

This file was deleted.

2 changes: 0 additions & 2 deletions packages/cursorless-engine/src/api/CursorlessEngineApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
ScopeProvider,
} from "@cursorless/common";
import type { CommandRunner } from "../CommandRunner";
import type { Snippets } from "../core/Snippets";
import type { StoredTargetMap } from "../core/StoredTargets";

export interface CursorlessEngine {
Expand All @@ -16,7 +15,6 @@ export interface CursorlessEngine {
customSpokenFormGenerator: CustomSpokenFormGenerator;
storedTargets: StoredTargetMap;
hatTokenMap: HatTokenMap;
snippets: Snippets;
injectIde: (ide: IDE | undefined) => void;
runIntegrationTests: () => Promise<void>;
addCommandRunnerDecorator: (
Expand Down
225 changes: 10 additions & 215 deletions packages/cursorless-engine/src/core/Snippets.ts
Original file line number Diff line number Diff line change
@@ -1,190 +1,12 @@
import { showError, Snippet, SnippetMap, walkFiles } from "@cursorless/common";
import { readFile, stat } from "fs/promises";
import { max } from "lodash-es";
import { join } from "pathe";
import { ide } from "../singletons/ide.singleton";
import { mergeStrict } from "../util/object";
import { mergeSnippets } from "./mergeSnippets";

const CURSORLESS_SNIPPETS_SUFFIX = ".cursorless-snippets";
const SNIPPET_DIR_REFRESH_INTERVAL_MS = 1000;

interface DirectoryErrorMessage {
directory: string;
errorMessage: string;
}
import { Snippet, SnippetMap } from "@cursorless/common";

/**
* Handles all cursorless snippets, including core, third-party and
* user-defined. Merges these collections and allows looking up snippets by
* name.
*/
export class Snippets {
private coreSnippets!: SnippetMap;
private thirdPartySnippets: Record<string, SnippetMap> = {};
private userSnippets!: SnippetMap[];

private mergedSnippets!: SnippetMap;

private userSnippetsDir?: string;

/**
* The maximum modification time of any snippet in user snippets dir.
*
* This variable will be set to -1 if no user snippets have yet been read or
* if the user snippets path has changed.
*
* This variable will be set to 0 if the user has no snippets dir configured and
* we've already set userSnippets to {}.
*/
private maxSnippetMtimeMs: number = -1;

/**
* If the user has misconfigured their snippet dir, then we keep track of it
* so that we can show them the error message if we can't find a snippet
* later, and so that we don't show them the same error message every time
* we try to poll the directory.
*/
private directoryErrorMessage: DirectoryErrorMessage | null | undefined =
null;

constructor() {
this.updateUserSnippetsPath();

this.updateUserSnippets = this.updateUserSnippets.bind(this);
this.registerThirdPartySnippets =
this.registerThirdPartySnippets.bind(this);

const timer = setInterval(
this.updateUserSnippets,
SNIPPET_DIR_REFRESH_INTERVAL_MS,
);

ide().disposeOnExit(
ide().configuration.onDidChangeConfiguration(() => {
if (this.updateUserSnippetsPath()) {
this.updateUserSnippets();
}
}),
{
dispose() {
clearInterval(timer);
},
},
);
}

async init() {
const extensionPath = ide().assetsRoot;
const snippetsDir = join(extensionPath, "cursorless-snippets");
const snippetFiles = await getSnippetPaths(snippetsDir);
this.coreSnippets = mergeStrict(
...(await Promise.all(
snippetFiles.map(async (path) =>
JSON.parse(await readFile(path, "utf8")),
),
)),
);
await this.updateUserSnippets();
}

/**
* Updates the userSnippetsDir field if it has change, returning a boolean
* indicating whether there was an update. If there was an update, resets the
* maxSnippetMtime to -1 to ensure snippet update.
* @returns Boolean indicating whether path has changed
*/
private updateUserSnippetsPath(): boolean {
const newUserSnippetsDir = ide().configuration.getOwnConfiguration(
"experimental.snippetsDir",
);

if (newUserSnippetsDir === this.userSnippetsDir) {
return false;
}

// Reset mtime to -1 so that next time we'll update the snippets
this.maxSnippetMtimeMs = -1;

this.userSnippetsDir = newUserSnippetsDir;

return true;
}

async updateUserSnippets() {
let snippetFiles: string[];
try {
snippetFiles = this.userSnippetsDir
? await getSnippetPaths(this.userSnippetsDir)
: [];
} catch (err) {
if (this.directoryErrorMessage?.directory !== this.userSnippetsDir) {
// NB: We suppress error messages once we've shown it the first time
// because we poll the directory every second and want to make sure we
// don't show the same error message repeatedly
const errorMessage = `Error with cursorless snippets dir "${
this.userSnippetsDir
}": ${(err as Error).message}`;

showError(ide().messages, "snippetsDirError", errorMessage);

this.directoryErrorMessage = {
directory: this.userSnippetsDir!,
errorMessage,
};
}

this.userSnippets = [];
this.mergeSnippets();

return;
}

this.directoryErrorMessage = null;

const maxSnippetMtime =
max(
(await Promise.all(snippetFiles.map((file) => stat(file)))).map(
(stat) => stat.mtimeMs,
),
) ?? 0;

if (maxSnippetMtime <= this.maxSnippetMtimeMs) {
return;
}

this.maxSnippetMtimeMs = maxSnippetMtime;

this.userSnippets = await Promise.all(
snippetFiles.map(async (path) => {
try {
const content = await readFile(path, "utf8");

if (content.length === 0) {
// Gracefully handle an empty file
return {};
}

return JSON.parse(content);
} catch (err) {
showError(
ide().messages,
"snippetsFileError",
`Error with cursorless snippets file "${path}": ${
(err as Error).message
}`,
);

// We don't want snippets from all files to stop working if there is
// a parse error in one file, so we just effectively ignore this file
// once we've shown an error message
return {};
}
}),
);

this.mergeSnippets();
}
export interface Snippets {
updateUserSnippets(): Promise<void>;

/**
* Allows extensions to register third-party snippets. Calling this function
Expand All @@ -195,22 +17,7 @@ export class Snippets {
* @param extensionId The id of the extension registering the snippets.
* @param snippets The snippets to be registered.
*/
registerThirdPartySnippets(extensionId: string, snippets: SnippetMap) {
this.thirdPartySnippets[extensionId] = snippets;
this.mergeSnippets();
}

/**
* Merge core, third-party, and user snippets, with precedence user > third
* party > core.
*/
private mergeSnippets() {
this.mergedSnippets = mergeSnippets(
this.coreSnippets,
this.thirdPartySnippets,
this.userSnippets,
);
}
registerThirdPartySnippets(extensionId: string, snippets: SnippetMap): void;

/**
* Looks in merged collection of snippets for a snippet with key
Expand All @@ -219,23 +26,11 @@ export class Snippets {
* @param snippetName The name of the snippet to look up
* @returns The named snippet
*/
getSnippetStrict(snippetName: string): Snippet {
const snippet = this.mergedSnippets[snippetName];

if (snippet == null) {
let errorMessage = `Couldn't find snippet ${snippetName}. `;
getSnippetStrict(snippetName: string): Snippet;

if (this.directoryErrorMessage != null) {
errorMessage += `This could be due to: ${this.directoryErrorMessage.errorMessage}.`;
}

throw Error(errorMessage);
}

return snippet;
}
}

function getSnippetPaths(snippetsDir: string) {
return walkFiles(snippetsDir, CURSORLESS_SNIPPETS_SUFFIX);
/**
* Opens a new snippet file in the users snippet directory.
* @param snippetName The name of the snippet
*/
openNewSnippetFile(snippetName: string): Promise<void>;
}
12 changes: 5 additions & 7 deletions packages/cursorless-engine/src/cursorlessEngine.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
Command,
CommandServerApi,
ensureCommandShape,
FileSystem,
Hats,
IDE,
ensureCommandShape,
ScopeProvider,
} from "@cursorless/common";
import {
Expand All @@ -13,7 +13,8 @@ import {
} from "./api/CursorlessEngineApi";
import { Debug } from "./core/Debug";
import { HatTokenMapImpl } from "./core/HatTokenMapImpl";
import { Snippets } from "./core/Snippets";
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
import type { Snippets } from "./core/Snippets";
import { StoredTargetMap } from "./core/StoredTargets";
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl";
Expand All @@ -30,24 +31,22 @@ import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
import { injectIde } from "./singletons/ide.singleton";
import { TreeSitter } from "./typings/TreeSitter";
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
import { DisabledSnippets } from "./disabledComponents/DisabledSnippets";

export async function createCursorlessEngine(
treeSitter: TreeSitter,
ide: IDE,
hats: Hats,
commandServerApi: CommandServerApi | null,
fileSystem: FileSystem,
snippets: Snippets = new DisabledSnippets(),
): Promise<CursorlessEngine> {
injectIde(ide);

const debug = new Debug(treeSitter);

const rangeUpdater = new RangeUpdater();

const snippets = new Snippets();
snippets.init();

const hatTokenMap = new HatTokenMapImpl(
rangeUpdater,
debug,
Expand Down Expand Up @@ -123,7 +122,6 @@ export async function createCursorlessEngine(
customSpokenFormGenerator,
storedTargets,
hatTokenMap,
snippets,
injectIde,
runIntegrationTests: () =>
runIntegrationTests(treeSitter, languageDefinitions),
Expand Down
Loading

0 comments on commit 850c1f9

Please sign in to comment.