When programming a vscode extension...
Is there a programmatic way to find the keybinding for a provided command?
I would like to be able to see if a user has updated the key mapping from default for a command so that the UI can display the up-to-date binding. (and if not, look up the default binding)
Here are the APIs I've looked into so far:
vscode.workspace.getConfiguration() - I cannot determine how to access the keybindings.json file / perform a lookup.
vscode.extensions.getExtension(name/id) allows access to the package.json, but not the command or keybinding override.
vscode.getCommands does not provide access to the keybinding values either...
You can get keybinding values from the keybindings.json file using NodeJS.
keybindings.json path on diferrent systems:
Windows: %APPDATA%\Code\User\keybindings.json
Mac: $HOME/Library/Application Support/Code/User/keybindings.json
Linux: $HOME/.config/Code/User/keybindings.json
To build the path you'll need to get Environment variables using process.env.{variableName}.
For example for MacOS it'll be:
var process = require('process');
//...
var keybindingsPath = process.env.HOME + "/Library/Application Support/Code/User/keybindings.json";
vscode.workspace.openTextDocument(keybindingsPath).then((document) => {
let text = document.getText();
//then use this JSON file for your needs
//...
});
Inspired by #nikita-kunevich answer Here is a code I use in autoit extension.
First it gets default keybindings from package.json, then it parses keybindings.json via JSON5 library (can't use JSON.parse() because file may contain comments) and replaces default keys with new keys.
//get keybindings
const keybindings = new Promise(resolve => {
//default keybindings
const data = require("../package.json").contributes.keybindings.reduce((a,b)=>(a[b.command]=b.key,a),{});
const parse = list => {
for(let i = 0; i < list.length; i++) {
if (list[i].command in data)
data[list[i].command] = list[i].key;
}
for(let i in data) {
//capitalize first letter
data[i] = data[i].replace(/\w+/g, w => (w.substring(0,1).toUpperCase()) + w.substring(1));
//add spaces around "+"
// data[i] = data[i].replace(/\+/g, " $& ");
}
Object.assign(keybindings, data);
resolve(data);
};
const path = {
windows: process.env.APPDATA + "/Code",
macos: process.env.HOME + "/Library/Application Support/Code",
linux: process.env.HOME + "/config/Code"
}[{
aix: "linux",
darwin: "macos",
freebsd: "linux",
linux: "linux",
openbsd: "linux",
sunos: "linux",
win32: "windows"
}[process.platform]||"windows"];
const file = ((process.env.VSCODE_PORTABLE ? process.env.VSCODE_PORTABLE + "/user-data/User/" : path) + "/User/keybindings.json")
.replace(/\//g, process.platform == "win32" ? "\\" : "/");
//read file
workspace.openTextDocument(file).then(doc => {
//we can't use JSON.parse() because file may contain comments
const JSON5 = require("json5").default;
parse(JSON5.parse(doc.getText()));
}).catch(er => {
parse([]);
});
});
To install JSON5 package use:
npm install json5
Usage:
keybindings.then(list => {
console.log(list);
});
Related
In package.json you can add a command to the File Explorer context menu.
In File Explorer you can select multiple files but my command only gets the last selected file URI as argument.
Can I get a list of all the selected files in the File Explorer?
You could look at my extension Find and Transform to see how I parse multiple files when an explorer context menu command is triggered. [There is some extra code in there because that command can be triggered by a keybinding or explorer/editor/tab menus so they have to be handled differently.]
let contextMenuCommandFile = vscode.commands.registerCommand('find-and-transform.searchInFile', async (...commandArgs) => {
let args = {};
if (commandArgs?.length === 1 && !(commandArgs[0] instanceof vscode.Uri)) { // if from keybinding
let argsArray = Object.entries(commandArgs[0]).filter(arg => {
return searchCommands.getKeys().includes(arg[0]);
});
Object.assign(args, Object.fromEntries(argsArray));
}
args.filesToInclude = await parseCommands.parseArgs(commandArgs, "file");
args.triggerSearch = true;
searchCommands.useSearchPanel(args);
});
context.subscriptions.push(contextMenuCommandFile);
You might be missing this async (...commandArgs) => { to get all available passed arguments into an array.
...commandArgs will be of length 1 if coming from a keybinding and length 2 if trigggered from the context menu no matter how many files were selected in the Explorer before right-clicking on one and choosing the command.
commandArgs[0] is the single file (i.e., the last file) on which I right-clicked.
commandsArgs[1] is itself an array of all the selected files in the explorer.
I then send that commandArgs array to be parsed (since I just need a comma-separated list of the files selected) to parseCommands.parseArgs() [okay, strangely-named function!).
The operative bit there is:
else if (commandArgs[1][0] instanceof vscode.Uri) { // explorer/context
for (const resource of commandArgs[1]) {
const thisResource = vscode.workspace.asRelativePath(resource.fsPath);
resources += `${ thisResource }, `;
}
resources = resources.substring(0, resources.length - 2); // strip ', ' off end
return resources;
}
How can I tell my extension to look first for &s_?
One solution would be to rename all variable to capital prefix S_, but this is not an option in my scenario.
I am working on an extension for Visual Studio Code(VSC). Variable are defined as followed:
&i_ Var_Six`
&s_ SampelTwo
&...
The implementation in VSC is done by "direct implementation" like that:
let provider1 = vscode.languages.registerCompletionItemProvider({ language: 'test' }, {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext) {
vscode.languages.registerCompletionItemProvider
var completions = new Array();
for (let knownElement of TestVar) {
completions.push(new vscode.CompletionItem(knownElement));
}
return completions;
}
});
context.subscriptions.push(provider1);
Now the problem I have is, that the autocompleting looks first for capital letters but also recognizes the & not as part of the variable.
What I want is that I can start typing "s_" and it would suggest all known variables starting with "&s_".
What VSC actually is suggesting for input “s_” is "i_Var_Six". This is because it looks for the capital letter “S” (camelcase) and the char “ _ ” it does not recognize the char “&” because it is defined as a “word separator”.
i'm not sure why & doesn't work, this is the closest way of implementing it, that i know of.
(read the comments)
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const TestVar: string[] = ['&_awd', '&_Awd', '&_213', '$_34', '$_23'];
let provider1 = vscode.languages.registerCompletionItemProvider(
{ language: 'plaintext' },
{
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
) {
var completions = [];
for (let knownElement of TestVar) {
const item = new vscode.CompletionItem(knownElement);
// i added this so that the first & sign gets removed,
// you might want to add a check so that this edit only gets added when the user typed a & sign.
item.additionalTextEdits = [
{ newText: '', range: new vscode.Range(new vscode.Position(position.line, position.character - 1), position), },
];
completions.push(item);
}
return completions;
},
},
'.',
'&' // add this so that the completions get triggered when the user types the & sign.
);
context.subscriptions.push(provider1);
}
I want to make a command like y!prefix [new_prefix] for servers who add my bot.
How can I do that?
Thanks!
First you have to make a json file to stock the prefix.
Next, make a command with this :
let prefixes = JSON.parse(fs.readFileSync("./JSON/prefix.json", "utf8"));
prefixes[message.guild.id] = {
prefixes: args[0]
};
fs.writeFile("./JSON/prefix.json", JSON.stringify(prefixes), (err) => {
if(err) console.log(err);
});
After, you just have to set the prefix for your commands in your main with something like this :
let prefixes = JSON.parse(fs.readFileSync("./JSON/prefix.json", "utf8"));
if (!prefixes[message.guild.id]) {
prefixes[message.guild.id] = {
prefixes: PREFIX,
};
}
let gprefix = prefixes[message.guild.id].prefixes;
I am creating a vscode extension that does some custom auto-completing of files paths.
I want to take what the user has typed, and if that value resolves to a folder in the workspace, I want to list all the files in that folder for auto-complete.
For example, given:
a workspace located at: /home/me/my-vs-project
with files:
/home/me/my-vs-project/assets/dog.png
/home/me/my-vs-project/assets/cat.jpeg
If I type in 'assets' or './assets' into vscode, the extension should be able to provide me an autocomplete list of:
'./assets/dog.png'
'./assets/cat.png'
Here's a snippet of the code that doesn't work (returns 0 results)..
let inputAsWorkspaceRelativeFolder = getInput(document, position); // for example, would return: '/home/me/my-vs-project/assets' for input of './assets'
let glob = inputAsWorkspaceRelativeFolder + '/*';
vscode.workspace.findFiles(glob, null, 100).then((uris: vscode.Uri[] ) => {
uris.forEach((uri: vscode.Uri) => {
console.log(uri);
});
});
For some reason, the above code is returning 0 uris though. Thoughts on how I have to format the glob to make this happen? and/or if there is a better approach?
I was able to do this using vscode.RelativePattern -- I'm sure I could've done it using generic GlobPatterns but im still not clear what the findFiles(..) consider the 'root' when matching files; RelativePattern is explicitly relative to the workspace root.
let workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(document.uri);
if (!workspaceFolder || document.isUntitled) {
return undefined;
}
// Workspace folder: /home/me/my-project
let workspaceFolderPath: string = workspaceFolder.uri.path;
let relativeSearchFolderPrefix = path.normalize(path.dirname(document.uri.path) + '/' + searchText);
relativeSearchFolderPrefix = path.relative(workspaceFolderPath, relativeSearchFolderPrefix);
let relativePattern: vscode.RelativePattern = new vscode.RelativePattern(
workspaceFolderPath,
relativeSearchFolderPrefix + '/**/*.{png,jpeg,jpg,gif}');
return vscode.workspace.findFiles(globPattern, null, 50).then((uris: vscode.Uri[] ) => {
let relativePaths: string[] = [];
uris.forEach((uri: vscode.Uri) => {
relativePaths.push(path.relative(current, uri.path));
});
// trivial custom function that turns an array of strings into CompletionItems
return getCompletionItems(relativePaths, vscode.CompletionItemKind.File);
});
😊👋🏻
I think you wronged the glob.
I found this intresting wiki about Glob pattern composition.
let inputAsWorkspaceRelativeFolder = 'asset'; // for example, would return: '/home/me/my-vs-project/assets' for input of './assets'
//https://github.com/ev3dev/vscode-ev3dev-browser/wiki/Glob-Patterns
let glob = '**/'+inputAsWorkspaceRelativeFolder+'/*.*';//or +'/{*.png,*.jpeg}';
Or you can use the node built-in fs
import * as fs from 'fs';
fs.readdir(inputAsWorkspaceRelativeFolder, (err, files: string[]) => {
files.forEach((file: path) => {
const uri = vscode.Uri.file(file);
console.log(uri);
});
});
More simple, if you want to get all the files in the asset folder and don't want to filter for extension.
Is it possible to jump to an existing symbol in file from Extension?
Something of the sort:
goToSymbol(symbol: string)
P.S. I don't want to provide a DocumentSymbolProvider. How to use existing ones?
async function getSymbols(document: TextDocument): Promise<DocumentSymbol[]> {
return await commands.executeCommand<DocumentSymbol[]>('vscode.executeDocumentSymbolProvider', document.uri) || [];
}
async function goToSymbol(document: TextDocument, symbolName: string) {
const symbols = await getSymbols(document);
const findSymbol = symbols.find(symbol => symbol.name === symbolName);
const activeTextEditor = window.activeTextEditor;
if (findSymbol && activeTextEditor) {
activeTextEditor.revealRange(findSymbol.range, vscode.TextEditorRevealType.AtTop);
activeTextEditor.selection = new Selection(findSymbol.range.start, findSymbol.range.start);
}
}
Note: the code above should be enough to go to symbol of 1 lvl.
Nested symbols accessible as .children (on every element of symbols)