Skip to content

Commit 1df8845

Browse files
authored
Merge pull request #5 from alternate-file/features/multi-dirname
Features/multi dirname
2 parents 39a1ff3 + bbbd537 commit 1df8845

File tree

16 files changed

+218
-50
lines changed

16 files changed

+218
-50
lines changed

.projections.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"src/*.ts": { "alternate": ["src/test/{}.test.ts", "src/{}.test.ts"] }
2+
"src/*.ts": { "alternate": ["src/{}.test.ts", "src/test/{}.test.ts"] }
33
}

.vscodeignore

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
.vscode/**
1+
.build.yml
2+
.gitignore
3+
.prettierignore
4+
.projections.json
25
.vscode-test/**
3-
out/test/**
6+
.vscode/**
7+
assets/screencasts
48
out/**/*.map
9+
out/test/**
510
src/**
6-
.gitignore
11+
test-project
712
tsconfig.json
8-
vsc-extension-quickstart.md
913
tslint.json
10-
yarn.lock
11-
test-project
12-
assets/screencasts
14+
vsc-extension-quickstart.md
15+
yarn.lock

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Support finding .projections.json in places other than the workspace.
13+
- Support multiple dirname parts
1314

1415
### Changed
1516

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ Each line should have the pattern for an implementation file as the key, and an
2727

2828
If your test paths have an extra directly in the middle of them, like with `app/some/path/__test__/file.test.js` with Jest, you can use `{dirname}` for the directory path and `{basename}` for the filename. You can do the same thing on the implementation side with the standard glob syntax: `**` to represent the directory path, and `*` to represent the filename, like `app/**/something/*.js`.
2929

30+
If your paths have more than two variable parts, that can work too! You can use multiple sets of `**`/`{dirname}` pairs, which allows you to do something like:
31+
32+
```json
33+
"apps/**/lib/**/*.ex": {
34+
"alternate": "apps/{dirname}/test/{dirname}/{basename}_test.exs"
35+
}
36+
```
37+
3038
### Multiple alternates
3139

3240
If your project is inconsistent about where specs go (it happens to the best of us), you can also pass an array to `alternate`. The extension will look for a file matching the first alternate, then the second, and so on. When you create an alternate file, it will always follow the first pattern.
@@ -119,4 +127,4 @@ Click the Debug button in the sidebar and run `Extension`
119127
- Support templates for auto-populating new files.
120128
- Automatically create default .projection.json files
121129
- Support all the transformations from Projectionist, not just `dirname` and `basename`.
122-
- Support the "type" attribute in `.projections.json`, and allow for lookup by filetype, like for "controller/view/template".
130+
- Support the "type" attribute in `.projections.json`, and allow for lookup by filetype, like for "`controller`/`view`/`template`".

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"alt": "./bin/cli.js"
2626
},
2727
"icon": "assets/icon.png",
28+
"galleryBanner": {
29+
"color": "#3771C8"
30+
},
2831
"activationEvents": [
2932
"onCommand:alternate.alternateFile",
3033
"onCommand:alternate.alternateFileInSplit",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"apps/**/lib/**/*.ex": {
3+
"alternate": "apps/{dirname}/test/{dirname}/{basename}_test.exs"
4+
}
5+
}

src/engine/AlternatePattern.test.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ describe("AlternatePattern", () => {
1010
{
1111
main: "app/{dirname}/{basename}.rb",
1212
alternate: "test/{dirname}/{basename}_spec.rb"
13+
},
14+
{
15+
main: "apps/{dirname}/lib/{dirname}/{basename}.ex",
16+
alternate: "apps/{dirname}/test/{dirname}/{basename}_test.exs"
1317
}
1418
];
1519

@@ -20,34 +24,44 @@ describe("AlternatePattern", () => {
2024
it("finds an implementation from a test", () => {
2125
expect(
2226
AlternatePattern.alternatePath(
23-
"src/components/__test__/Foo.test.ts",
27+
"/project/src/components/__test__/Foo.test.ts",
2428
projectionsPath
2529
)(patterns[0])
2630
).toBe("/project/src/components/Foo.ts");
2731
});
2832

2933
it("finds alternate for short path", () => {
3034
expect(
31-
AlternatePattern.alternatePath("app/foo.rb", projectionsPath)(
35+
AlternatePattern.alternatePath("/project/app/foo.rb", projectionsPath)(
3236
patterns[1]
3337
)
3438
).toBe("/project/test/foo_spec.rb");
3539
});
3640

3741
it("finds ts specs", () => {
3842
expect(
39-
AlternatePattern.alternatePath("./src/foo/bar.ts", projectionsPath)(
40-
patterns[0]
41-
)
43+
AlternatePattern.alternatePath(
44+
"/project/src/foo/bar.ts",
45+
projectionsPath
46+
)(patterns[0])
4247
).toBe("/project/src/foo/__test__/bar.test.ts");
4348
});
4449

4550
it("returns null for non-matches", () => {
4651
expect(
47-
AlternatePattern.alternatePath("src/foo.rb", projectionsPath)(
52+
AlternatePattern.alternatePath("/project/src/foo.rb", projectionsPath)(
4853
patterns[0]
4954
)
5055
).toBe(null);
5156
});
57+
58+
it("finds a match with multiple dirnames", () => {
59+
const path = AlternatePattern.alternatePath(
60+
"/project/apps/my_app/lib/accounts/user.ex",
61+
projectionsPath
62+
)(patterns[2]);
63+
64+
expect(path).toBe("/project/apps/my_app/test/accounts/user_test.exs");
65+
});
5266
});
5367
});

src/engine/AlternatePattern.ts

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as path from "path";
2+
import * as utils from "./utils";
3+
import * as Result from "../result/Result";
24

35
/**
46
* A computer-friendly representation of paths for switching between alternate files.
@@ -8,23 +10,28 @@ export interface t {
810
alternate: string;
911
}
1012

11-
const slash = "/";
12-
const backslash = "\\\\";
13-
const anyBackslashRegex = new RegExp(backslash, "g");
13+
const slash = "[/\\]";
14+
const notSlash = "[^/\\]";
15+
const escapedSlash = "[/\\\\]";
1416

15-
const dirnameRegex = new RegExp(
16-
`{dirname}(?:${slash}|${backslash}${backslash})`,
17-
"g"
18-
);
17+
const anyBackslashRegex = /\\/g;
18+
const escapedBackslash = "\\\\";
19+
20+
const transformationPattern = "{([^{}]+)}";
21+
22+
const dirnameRegex = new RegExp(`{dirname}${escapedSlash}`, "g");
1923
const basenameRegex = /{basename}/g;
2024

21-
const dirnamePattern = `(?:(.+)[${slash}${backslash}])?`;
22-
const basenamePattern = `([^${slash}${backslash}]+)`;
25+
const dirnamePattern = `(?:(.+)${slash})?`;
26+
const basenamePattern = `(${notSlash}+)`;
2327

2428
/**
25-
* Use an AlternatePath to find a possible alternate path for a file.
26-
* @param path
27-
* @param projectionsPath
29+
* Given a filePath and an AlternatePath, calculate if the filePath matches
30+
* a pattern, and if it does, calculate what the matching alternate file path
31+
* would be.
32+
* @param path - the absolute path to the file
33+
* @param projectionsPath - the absolute path to the projections file
34+
* @param alternatePath - the AlternatePath object to match against.
2835
*/
2936
export const alternatePath = (path: string, projectionsPath: string) => ({
3037
main,
@@ -45,31 +52,101 @@ const alternatePathForSide = (
4552
alternatePattern
4653
);
4754

48-
const regex = patternToRegex(absolutePattern);
55+
const matchResult = matchPatternToPath(absolutePattern, filePath);
56+
if (Result.isError(matchResult)) return null;
57+
58+
const pathMatches = matchResult.ok;
59+
const transformations = patternToTransformations(absolutePattern);
60+
61+
return fillPattern(transformations, pathMatches, absoluteAlternatePattern);
62+
};
63+
64+
/**
65+
* Take the available transformation names and match values, and use them to fill up the alternate pattern.
66+
* @param transformations
67+
* @param matches
68+
* @param alternatePattern
69+
* @returns A complete file path.
70+
*/
71+
const fillPattern = (
72+
transformations: string[],
73+
matches: string[],
74+
alternatePattern: string
75+
): string => {
76+
const filledPath = utils
77+
.zip(transformations, matches)
78+
.reduce(
79+
(alternatePattern: string, [transformation, match]: [string, string]) =>
80+
alternatePattern.replace(`{${transformation}}`, match || ""),
81+
alternatePattern
82+
);
83+
84+
return path.normalize(filledPath);
85+
};
86+
87+
/**
88+
* Extract a list of transformations from a pattern, to be zipped with their matches.
89+
* @param pathPattern
90+
* @returns list of transformation names
91+
*/
92+
const patternToTransformations = (pathPattern: string) => {
93+
const regex = new RegExp(transformationPattern, "g");
94+
95+
const transformations: string[] = [];
96+
let matches: RegExpExecArray | null;
97+
98+
while ((matches = regex.exec(pathPattern)) !== null) {
99+
transformations.push(matches[1]);
100+
}
101+
102+
return transformations;
103+
};
104+
105+
/**
106+
* Take a path pattern string, and use it to try to pull out matches from the file path.
107+
* @param pathPattern - String to be converted to regex
108+
* @param filePath - Current file
109+
* @returns Ok(matches) | Error(null) if no matches
110+
*/
111+
const matchPatternToPath = (
112+
pathPattern: string,
113+
filePath: string
114+
): Result.Result<string[], null> => {
115+
const regex = patternToRegex(pathPattern);
49116
const matches = filePath.match(regex);
50117

51-
if (!matches || !matches[2]) return null;
118+
if (!matches || !matches[2]) return Result.error(null);
52119

53-
const dirname = matches[1];
54-
const basename = matches[2];
120+
const pathMatches = matches.slice(1);
55121

56-
return path.normalize(
57-
absoluteAlternatePattern
58-
.replace(dirnameRegex, dirname ? `${dirname}/` : "")
59-
.replace(basenameRegex, basename)
60-
);
122+
return Result.ok(pathMatches);
61123
};
62124

125+
/**
126+
* Take a projections pattern, and convert it to a regex that will extract the variable parts.
127+
*/
63128
const patternToRegex = (pathPattern: string): RegExp => {
64129
const regexPattern = pathPattern
65130
.replace(dirnameRegex, dirnamePattern)
66131
.replace(basenameRegex, basenamePattern);
67-
return new RegExp(regexPattern);
132+
133+
const escapedPattern = escapeBackslashes(regexPattern);
134+
135+
return new RegExp(escapedPattern);
68136
};
69137

138+
/**
139+
* Append a pattern to the absolute path of the projections file
140+
* @param projectionsPath - absolute path to the projections file
141+
* @param filePattern -
142+
*/
70143
const combinePaths = (projectionsPath: string, filePattern: string): string => {
71144
const projectionsDir = path.dirname(projectionsPath);
72-
const fullPath = path.resolve(projectionsDir, filePattern);
73-
74-
return fullPath.replace(anyBackslashRegex, backslash);
145+
return path.resolve(projectionsDir, filePattern);
75146
};
147+
148+
/**
149+
* Escape backslashes before converting a string to a regex.
150+
*/
151+
const escapeBackslashes = (pattern: string): string =>
152+
pattern.replace(anyBackslashRegex, escapedBackslash);

src/engine/File.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,15 @@ export const readFile = (path: string): Result.P<string, any> =>
8181
* Wrap a JSON parse in a Result.
8282
* @returns Ok(body)
8383
*/
84-
export const parseJson = <T>(data: string): Result.Result<T, any> => {
84+
export const parseJson = <T>(
85+
data: string,
86+
fileName?: string
87+
): Result.Result<T, any> => {
8588
try {
8689
return Result.ok(JSON.parse(data));
8790
} catch (e) {
88-
return Result.error(e);
91+
const message = `Couldn't parse ${fileName || "file"}: ${e.message}`;
92+
return Result.error(message);
8993
}
9094
};
9195

src/engine/Projections.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const readProjections = async (
121121
projectionsPath,
122122
File.readFile,
123123
Result.mapOk((data: string): string => (data === "" ? "{}" : data)),
124-
Result.chainOk((x: string) => File.parseJson<t>(x)),
124+
Result.chainOk((x: string) => File.parseJson<t>(x, projectionsPath)),
125125
Result.mapError((error: string) => ({
126126
startingFile: projectionsPath,
127127
message: error
@@ -184,7 +184,7 @@ const mainPathToAlternate = (path: string): string => {
184184

185185
const taggedPath = /\*\*/.test(path) ? path : path.replace("*", "**/*");
186186

187-
return taggedPath.replace("**", "{dirname}").replace("*", "{basename}");
187+
return taggedPath.replace(/\*\*/g, "{dirname}").replace("*", "{basename}");
188188
};
189189

190190
const alternatePathToAlternate = (path: string): string => {

0 commit comments

Comments
 (0)