Repository: microsoft/vscode (TypeScript)
Date: 2021-01-12 13:56:19+00:00
Task type: Feature

Issue:
Add relative directory/filepath support to snippets
- VSCode Version: Code 1.17.1 (1e9d36539b0ae51ac09b9d4673ebea4e447e5353, 2017-10-10T14:24:41.632Z)
- OS Version: Windows_NT ia32 6.3.9600

Currently, snippets allow you to resolve the current directory or filepath with `TM_DIRECTORY` and `TM_FILEPATH` respectively. It would be great to be able to get the path for these relative to the root folder, whether directly (e.g. `TM_DIRECTORY_REL`) or by providing a variable with the root folder.

I'm trying to write a snippet to create a new C# file. Visual Studio will auto-populate the file with `namespace Directory.Structure.Here`, but that doesn't seem possible in Visual Studio Code right now.
Add relative directory/filepath support to snippets
- VSCode Version: Code 1.17.1 (1e9d36539b0ae51ac09b9d4673ebea4e447e5353, 2017-10-10T14:24:41.632Z)
- OS Version: Windows_NT ia32 6.3.9600

Currently, snippets allow you to resolve the current directory or filepath with `TM_DIRECTORY` and `TM_FILEPATH` respectively. It would be great to be able to get the path for these relative to the root folder, whether directly (e.g. `TM_DIRECTORY_REL`) or by providing a variable with the root folder.

I'm trying to write a snippet to create a new C# file. Visual Studio will auto-populate the file with `namespace Directory.Structure.Here`, but that doesn't seem possible in Visual Studio Code right now.


Issue discussion:
We have added [*Variable Transformations*](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variable-transforms) for thing like this. Please give it a try
I'm not sure how that would help in this case. If `TM_DIRECTORY` resolves to `C:\dev\projects\my_projects\secret_projects\secret_project1\features\secret_feature`, how can I know where the project begins? If I have the project root at `secret_project1`, I would expect the namespace to be  `secret_project1.features.secret_feature`, but I don't see any way of reliably generating that without making assumptions about base folder structure.
I could also see this used for other situations, like generating a JavaScript file that imports a common file, like `const myCommonLib = require('../../../common.js');`. In this case, if I had the relative directory, I could easily use the Variable Transformations to generate `../../../`
> how can I know where the project begins?

Well, how would we know? Do you suggest to take the current folder? What if your project start one level deeper, like `src` and `test`? I like the idea of having a variable that resolves the active (workspace) folder but I don't know if it will help you. Tho, it might make it easier to craft a snippet with  transforms
Sorry, when I said project root, I meant workspace root.

You're right, it wouldn't solve every situation, but I think it could be handy to have.
Any idea how to make this works using variable transformations?
@Spielberg, if you hardcode root foder, you can use the next transformation:
`"const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",`
@jrieken "variable that resolves the active (workspace) folder" is definitely missing. How about 'custom' variable that could read string from another file? Usually, if source root is not the relative folder, it is written in some (json, xml..) file in relative folder. Beside resolving this issue, probably that new variable would provide many more options in snippets.
Workspace-related snippet variables would be very handy for plenty of cases, I was surprised they weren't included already when I went looking for them
Add a new variable `TM_WORKSPACE_ROOT` pointing to the root path of the workspace will be awesome!
> > how can I know where the project begins?
> 
> Well, how would we know?

I would expect the output to be the same as from the _"File: Copy Relative Path of Active File"_ command. So can't that be used for this snippet too?
> Add a new variable `TM_WORKSPACE_ROOT` pointing to the root path of the workspace will be awesome!

This would be great. If we could also get a `TM_DIRECTORY_RELATIVE_WORKSPACE_ROOT`, that would be super amazing, and provide a better basis for doing what the OP is trying to do. It would only contain the portion of the path after the workspace root of the directory, so it would be the relative path of the directory of the current document.

Honestly, I can't believe something like this wasn't one of the very first variables ever made for snippets. How can so many programmers be relying on an editor that doesn't even help you write your boilerplate code with the correct namespace? The whole point of programming is to automate away the mindless repetitive stuff. Actually the entire way snippets were conceived was ridiculous from the start. Why was the TextMate snippet syntax chosen? Were they intentionally trying to make it as annoying and useless as possible? I mean really, we're supposed to write snippets as values in a JSON format? We are programmers, just lets us write code in an actual programming language that can output the text as a string, with some helpful pre-made variables we can concatenate in the outputted strings. Then we would have the ability to actually write code to format/produce the text rather than to have rely on cryptic transformations and regular expressions.

But it gets even worse from there. Why should the OP have to go out of their way to make and then activate a snippet at all, when all they are really doing is creating a new file? They already chose to create the new file, and to give it an appropriate name and file extension. That should be enough information for VS Code to be able to activate an appropriate template for the most common case you would be creating a new file of that file type for, as the OP is describing, no snippets necessary. We also shouldn't need to go searching through user made extensions to try to find something like that. This makes it seem like MS is intentionally leaving out the most obvious features to prevent it from competing with Visual Studio.
When editing other settings for VS Code, you can sometimes use [`${workspaceFolder}` and `${relativeFileDirname}` variables](https://code.visualstudio.com/docs/editor/variables-reference). That is what we need here. I tried just using them, but they didn't work for me here.
> @Spielberg, if you hardcode root foder, you can use the next transformation:
> `"const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",`

I'm really terrible at transformations and regex. Could you please provide a similar transformation that inserts the names of the directories after src/ that would produce code in this format:
`namespace SomeHardCodedThing\Directory\Structure\After\Src;`
2 years ago I asked [this question on Stackoverflow](https://stackoverflow.com/questions/48366014/how-to-get-the-base-directory-in-visual-studio-code-snippet/48366340#48366340) about getting the base directory (not the full path). 

Having the current directory name (and not the full path) seems to be in demand. Sure people are giving their upvote for the transform but what we really want is a simple, relatively short variable to get it. The snippets are not that obvious with these transforms repeated everywhere and not the simplest thing to maintain.

Please, consider adding some variables like the ones mentioned here.
I don't mind trying to put together a PR, but I tend to have a hard time getting started in a new code base (new for me). Is someone able to point me both to the code that makes the [${workspaceFolder} and ${relativeFileDirname} variables available](https://code.visualstudio.com/docs/editor/variables-reference) in other parts of VS Code, and the code that makes [certain variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables) available to snippets?
Did a quick search and it is likely to be here: https://github.com/microsoft/vscode/blob/master/src/vs/workbench/services/configurationResolver/common/variableResolver.ts#L222-L290
EDIT: and here https://github.com/microsoft/vscode/blob/master/src/vs/editor/contrib/snippet/snippetVariables.ts
I just realized something. Even if I had a variable that provided the relative path after project directory, I would still need to use the incomprehensible variable transformations feature to remove 'src/' from the start of that variable, so it wouldn't actually help me.
I'm trying to make a snippet always locate a Theme.js file no matter where it's created. Is there any way to say something like `$RELATIVE(/src/theme)` and have it automatically produce an output like `../theme` if we're located in `src/components/ActionBar.js` when triggering the snippet?
I borrowed the transformation from @DVDima and it worked pretty well for my case. It's not perfect because neighboring components will use `../` and then come back down the path, but that [could be solved by a linter.](https://github.com/benmosher/eslint-plugin-import/blob/HEAD/docs/rules/no-useless-path-segments.md)

Okay, let's say you have:

- Workspace path: `~/git/my-folder`
- Path to relative library I want to create a snippet for: `~/git/my-folder/src/common/lib.js`
- Current file: `~/git/my-folder/src/special/cool/app/file.js`

The following snippet should work up to 6 levels deep:

```json
{
  "Import common lib": {
    "prefix": ["import lib from common", "common-lib", "lib"],
    "body": ["import lib from '${TM_DIRECTORY/.*src(\\/[^\\/]*)(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?/${1:+../}${2:+../}${3:+../}${4:+../}${5:+../}${6:+../}/}common/lib.js'"]
  }
}
```

Upon running the snippet, you should get `import lib from '../../common/lib.js'`

### Caveats

- The regex is assuming forward slashes in the directory—would need to be adjusted to backslashes on Windows or adjusted to be agnostic. A backward slash escaped in regex in JSON looks like `\\\\` and would replace `\\/` in all instances above.
- This will work 6-subdirectories deep. If you need fewer or more, you can copy-paste the optional groups `([…])?` and then place more variable insertions `${N:+../}`.

### Explanation

The regex is just trying to find as many groups beyond the common directory (`src` in the example above) and replacing them with `../`. It's very simple. Having to escape the backslashes and putting it into JSON makes it hard to read. Basically we're saying:

`in string TM_DIRECTORY, replace .*app(/[^/]*) with ${1:+../}/}common/lib.js`
 
Transforming `any-garbage-here/app/somedir` → `../common/lib.js`

Then the group for further directories needs to be repeated until you've reached what's practical for your project.

Hope this helps!
> "const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",
I want to do something similar but if I use this one. It results in 

>const myCommonLib = require('../,../,,common.js');

No idea how but I am getting those commas. Is there a way to remove them?
We have added [*Variable Transformations*](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variable-transforms) for thing like this. Please give it a try
I'm not sure how that would help in this case. If `TM_DIRECTORY` resolves to `C:\dev\projects\my_projects\secret_projects\secret_project1\features\secret_feature`, how can I know where the project begins? If I have the project root at `secret_project1`, I would expect the namespace to be  `secret_project1.features.secret_feature`, but I don't see any way of reliably generating that without making assumptions about base folder structure.
I could also see this used for other situations, like generating a JavaScript file that imports a common file, like `const myCommonLib = require('../../../common.js');`. In this case, if I had the relative directory, I could easily use the Variable Transformations to generate `../../../`
> how can I know where the project begins?

Well, how would we know? Do you suggest to take the current folder? What if your project start one level deeper, like `src` and `test`? I like the idea of having a variable that resolves the active (workspace) folder but I don't know if it will help you. Tho, it might make it easier to craft a snippet with  transforms
Sorry, when I said project root, I meant workspace root.

You're right, it wouldn't solve every situation, but I think it could be handy to have.
Any idea how to make this works using variable transformations?
@Spielberg, if you hardcode root foder, you can use the next transformation:
`"const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",`
@jrieken "variable that resolves the active (workspace) folder" is definitely missing. How about 'custom' variable that could read string from another file? Usually, if source root is not the relative folder, it is written in some (json, xml..) file in relative folder. Beside resolving this issue, probably that new variable would provide many more options in snippets.
Workspace-related snippet variables would be very handy for plenty of cases, I was surprised they weren't included already when I went looking for them
Add a new variable `TM_WORKSPACE_ROOT` pointing to the root path of the workspace will be awesome!
> > how can I know where the project begins?
> 
> Well, how would we know?

I would expect the output to be the same as from the _"File: Copy Relative Path of Active File"_ command. So can't that be used for this snippet too?
> Add a new variable `TM_WORKSPACE_ROOT` pointing to the root path of the workspace will be awesome!

This would be great. If we could also get a `TM_DIRECTORY_RELATIVE_WORKSPACE_ROOT`, that would be super amazing, and provide a better basis for doing what the OP is trying to do. It would only contain the portion of the path after the workspace root of the directory, so it would be the relative path of the directory of the current document.

Honestly, I can't believe something like this wasn't one of the very first variables ever made for snippets. How can so many programmers be relying on an editor that doesn't even help you write your boilerplate code with the correct namespace? The whole point of programming is to automate away the mindless repetitive stuff. Actually the entire way snippets were conceived was ridiculous from the start. Why was the TextMate snippet syntax chosen? Were they intentionally trying to make it as annoying and useless as possible? I mean really, we're supposed to write snippets as values in a JSON format? We are programmers, just lets us write code in an actual programming language that can output the text as a string, with some helpful pre-made variables we can concatenate in the outputted strings. Then we would have the ability to actually write code to format/produce the text rather than to have rely on cryptic transformations and regular expressions.

But it gets even worse from there. Why should the OP have to go out of their way to make and then activate a snippet at all, when all they are really doing is creating a new file? They already chose to create the new file, and to give it an appropriate name and file extension. That should be enough information for VS Code to be able to activate an appropriate template for the most common case you would be creating a new file of that file type for, as the OP is describing, no snippets necessary. We also shouldn't need to go searching through user made extensions to try to find something like that. This makes it seem like MS is intentionally leaving out the most obvious features to prevent it from competing with Visual Studio.
When editing other settings for VS Code, you can sometimes use [`${workspaceFolder}` and `${relativeFileDirname}` variables](https://code.visualstudio.com/docs/editor/variables-reference). That is what we need here. I tried just using them, but they didn't work for me here.
> @Spielberg, if you hardcode root foder, you can use the next transformation:
> `"const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",`

I'm really terrible at transformations and regex. Could you please provide a similar transformation that inserts the names of the directories after src/ that would produce code in this format:
`namespace SomeHardCodedThing\Directory\Structure\After\Src;`
2 years ago I asked [this question on Stackoverflow](https://stackoverflow.com/questions/48366014/how-to-get-the-base-directory-in-visual-studio-code-snippet/48366340#48366340) about getting the base directory (not the full path). 

Having the current directory name (and not the full path) seems to be in demand. Sure people are giving their upvote for the transform but what we really want is a simple, relatively short variable to get it. The snippets are not that obvious with these transforms repeated everywhere and not the simplest thing to maintain.

Please, consider adding some variables like the ones mentioned here.
I don't mind trying to put together a PR, but I tend to have a hard time getting started in a new code base (new for me). Is someone able to point me both to the code that makes the [${workspaceFolder} and ${relativeFileDirname} variables available](https://code.visualstudio.com/docs/editor/variables-reference) in other parts of VS Code, and the code that makes [certain variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables) available to snippets?
Did a quick search and it is likely to be here: https://github.com/microsoft/vscode/blob/master/src/vs/workbench/services/configurationResolver/common/variableResolver.ts#L222-L290
EDIT: and here https://github.com/microsoft/vscode/blob/master/src/vs/editor/contrib/snippet/snippetVariables.ts
I just realized something. Even if I had a variable that provided the relative path after project directory, I would still need to use the incomprehensible variable transformations feature to remove 'src/' from the start of that variable, so it wouldn't actually help me.
I'm trying to make a snippet always locate a Theme.js file no matter where it's created. Is there any way to say something like `$RELATIVE(/src/theme)` and have it automatically produce an output like `../theme` if we're located in `src/components/ActionBar.js` when triggering the snippet?
I borrowed the transformation from @DVDima and it worked pretty well for my case. It's not perfect because neighboring components will use `../` and then come back down the path, but that [could be solved by a linter.](https://github.com/benmosher/eslint-plugin-import/blob/HEAD/docs/rules/no-useless-path-segments.md)

Okay, let's say you have:

- Workspace path: `~/git/my-folder`
- Path to relative library I want to create a snippet for: `~/git/my-folder/src/common/lib.js`
- Current file: `~/git/my-folder/src/special/cool/app/file.js`

The following snippet should work up to 6 levels deep:

```json
{
  "Import common lib": {
    "prefix": ["import lib from common", "common-lib", "lib"],
    "body": ["import lib from '${TM_DIRECTORY/.*src(\\/[^\\/]*)(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?(\\/[^\\/]*)?/${1:+../}${2:+../}${3:+../}${4:+../}${5:+../}${6:+../}/}common/lib.js'"]
  }
}
```

Upon running the snippet, you should get `import lib from '../../common/lib.js'`

### Caveats

- The regex is assuming forward slashes in the directory—would need to be adjusted to backslashes on Windows or adjusted to be agnostic. A backward slash escaped in regex in JSON looks like `\\\\` and would replace `\\/` in all instances above.
- This will work 6-subdirectories deep. If you need fewer or more, you can copy-paste the optional groups `([…])?` and then place more variable insertions `${N:+../}`.

### Explanation

The regex is just trying to find as many groups beyond the common directory (`src` in the example above) and replacing them with `../`. It's very simple. Having to escape the backslashes and putting it into JSON makes it hard to read. Basically we're saying:

`in string TM_DIRECTORY, replace .*app(/[^/]*) with ${1:+../}/}common/lib.js`
 
Transforming `any-garbage-here/app/somedir` → `../common/lib.js`

Then the group for further directories needs to be repeated until you've reached what's practical for your project.

Hope this helps!
> "const myCommonLib = require('${TM_DIRECTORY/.*src(\\\\[^\\\\]*)(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?(\\\\[^\\\\]*)?/${1:+../}${2:+../}${3:+../}${4:+../}/}common.js');",
I want to do something similar but if I use this one. It results in 

>const myCommonLib = require('../,../,,common.js');

No idea how but I am getting those commas. Is there a way to remove them?

Solution patch:
diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts
--- a/src/vs/editor/contrib/snippet/snippetVariables.ts
+++ b/src/vs/editor/contrib/snippet/snippetVariables.ts
@@ -43,6 +43,7 @@ export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze(
 	'TM_FILENAME_BASE': true,
 	'TM_DIRECTORY': true,
 	'TM_FILEPATH': true,
+	'RELATIVE_FILEPATH': true,
 	'BLOCK_COMMENT_START': true,
 	'BLOCK_COMMENT_END': true,
 	'LINE_COMMENT': true,
@@ -179,6 +180,8 @@ export class ModelBasedVariableResolver implements VariableResolver {
 
 		} else if (name === 'TM_FILEPATH' && this._labelService) {
 			return this._labelService.getUriLabel(this._model.uri);
+		} else if (name === 'RELATIVE_FILEPATH' && this._labelService) {
+			return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true });
 		}
 
 		return undefined;


Test patch:
diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
--- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
+++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
@@ -15,6 +15,7 @@ import { mock } from 'vs/base/test/common/mock';
 import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
 import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
 import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
+import { sep } from 'vs/base/common/path';
 
 suite('Snippet Variables Resolver', function () {
 
@@ -339,4 +340,49 @@ suite('Snippet Variables Resolver', function () {
 			assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/');
 		}
 	});
+
+	test('Add RELATIVE_FILEPATH snippet variable #114208', function () {
+
+		let resolver: VariableResolver;
+
+		// Mock a label service (only coded for file uris)
+		const workspaceLabelService = ((rootPath: string): ILabelService => {
+			const labelService = new class extends mock<ILabelService>() {
+				getUriLabel(uri: URI, options: { relative?: boolean } = {}) {
+					const rootFsPath = URI.file(rootPath).fsPath + sep;
+					const fsPath = uri.fsPath;
+					if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) {
+						return fsPath.substring(rootFsPath.length);
+					}
+					return fsPath;
+				}
+			};
+			return labelService;
+		});
+
+		const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt'));
+
+		// empty workspace
+		resolver = new ModelBasedVariableResolver(
+			workspaceLabelService(''),
+			model
+		);
+
+		if (!isWindows) {
+			assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt');
+		} else {
+			assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt');
+		}
+
+		// single folder workspace
+		resolver = new ModelBasedVariableResolver(
+			workspaceLabelService('/foo'),
+			model
+		);
+		if (!isWindows) {
+			assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt');
+		} else {
+			assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt');
+		}
+	});
 });