VSCode extension - how to alter file's text - visual-studio-code

I have an extension that grabs the open file's text and alters it. Once the text is altered, how do I put it back into the file that is displayed in VSCode?
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "myExtension" is now active!');
console.log(process.versions);
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('extension.myExtension', () => {
// The code you place here will be executed every time your command is executed
let activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
let text = activeEditor.document.getText();
getAsyncApi(text).then((textToInsertIntoDoc) => {
let finaldoc = insertTextIntoDoc(text, textToInsertIntoDoc);
// not what I want - just used to see new text
vscode.window.showInformationMessage(textToInsertIntoDoc);
});
});
context.subscriptions.push(disposable);
}

The API you can use here is TextEditor.edit, whose definition is
edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean>;
It asks for a callback as the first parameter and in the callback, you can make edits to the document by visiting editBuilder.
I put a sample extension in https://github.com/Microsoft/vscode-extension-samples/tree/master/document-editing-sample which reverses the content in current selection, which is basically a simple use TextEditor.edit.

This is a revision of the main function in Rebornix's extension sample (included with the set of Microsoft extension samples) that handles the selection issues you raised. It reverses the content of the selection(s) (leaving the selections) or if a selection is empty it will reverse the word under the cursor at that selection without leaving anything selected. It often makes sense to leave a selection, but you can add code to remove selection.
let disposable = vscode.commands.registerCommand('extension.reverseWord', function () {
// Get the active text editor
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
editor.edit(editBuilder => {
editor.selections.forEach(sel => {
const range = sel.isEmpty ? document.getWordRangeAtPosition(sel.start) || sel : sel;
let word = document.getText(range);
let reversed = word.split('').reverse().join('');
editBuilder.replace(range, reversed);
})
}) // apply the (accumulated) replacement(s) (if multiple cursors/selections)
}
});
Admittedly, while I could remove a single selection by setting .selection to a new empty selection that doesn't seem to work with .selections[i]. But you can make multiple changes without having selections in the first place.
What you don't want to do is make a selection through code just to alter text through code. Users make selections, you don't (unless the end purpose of the function is to make a selection).
I came to this question looking for a way to apply a textEdit[] array (which is normally returned by a provideDocumentRangeFormattingEdits callback function). If you build changes in the array you can apply them to your document in your own function:
const { activeTextEditor } = vscode.window;
if (activeTextEditor) {
const { document } = activeTextEditor;
if (document) {
/*
build your textEdits similarly to the above with insert, delete, replace
but not within an editBuilder arrow function
const textEdits: vscode.TextEdit[] = [];
textEdits.push(vscode.TextEdit.replace(...));
textEdits.push(vscode.TextEdit.insert(...));
*/
const workEdits = new vscode.WorkspaceEdit();
workEdits.set(document.uri, textEdits); // give the edits
vscode.workspace.applyEdit(workEdits); // apply the edits
}
}
So that's another way to apply edits to a document. Even though I got the editBuilder sample to work correctly without selecting text, I have had problems with selections in other cases. WorkspaceEdit doesn't select the changes.

Here is the code snippet that will solve your issue :
activeEditor.edit((selectedText) => {
selectedText.replace(activeEditor.selection, newText);
})

Due to the issues I commented about in the above answer, I ended up writing a quick function that does multi-cursor friendly insert, and if the selection was empty, then it does not leave the inserted text selected afterwards (i.e. it has the same intuitive behavior as if you had pressed CTRL + V, or typed text on the keyboard, etc.)
Invoking it is simple:
// x is the cursor index, it can be safely ignored if you don't need it.
InsertText(x => 'Hello World');
Implementation:
function InsertText(getText: (i:number) => string, i: number = 0, wasEmpty: boolean = false) {
let activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) { return; }
let sels = activeEditor.selections;
if (i > 0 && wasEmpty)
{
sels[i - 1] = new vscode.Selection(sels[i - 1].end, sels[i - 1].end);
activeEditor.selections = sels; // required or the selection updates will be ignored! 😱
}
if (i < 0 || i >= sels.length) { return; }
let isEmpty = sels[i].isEmpty;
activeEditor.edit(edit => edit.replace(sels[i], getText(i))).then(x => {
InsertText(getText, i + 1, isEmpty);
});
}

let strContent = "hello world";
const edit = new vscode.WorkspaceEdit();
edit.insert(YOUR_URI, new vscode.Position(0, 0), strContent);
let success = await vscode.workspace.applyEdit(edit);

Related

Insert predefined text at cursor position

I want to insert some text at the cursor's position but I didn't find the required code at the API docs.
Is there any function that I could put the parameters within, which solves my problem? This is just for testing.
import * as vscode from 'vscode';
var fs = require('fs');
var flow = require('xml-flow');
var inFile = fs.createReadStream('./your-xml-file.xml');
var xmlStream = flow(inFile);
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "intrexx-js-lib" is now active!');
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('intrexx-js-lib.start', () => {
const editor = vscode.window.activeTextEditor;
if(!editor){
vscode.window.showErrorMessage("Editor does not exist!");
return;
}
if (editor.selection.isEmpty) {
const position:vscode.Position = editor.selection.active;
//vscode.window.showInformationMessage(`line: ${position.line} character: ${position.character}`);
}
});
context.subscriptions.push(disposable);
}
// this method is called when your extension is deactivated
export function deactivate() {}
Although rioV8's code works, if any text is selected it will overwrite that text. If you want to ensure that no text is deleted by the insert operation, use edit.insert instead. For rioV8's second example, that means doing this:
vscode.commands.registerTextEditorCommand('intrexx-js-lib.start', (editor, edit) => {
editor.selections.forEach((selection, i) => {
let text = "FooBar " + i;
edit.insert(selection.active, text); // insert at current cursor
})
});
Use a different command register function, it gives you a TextEditorEdit:
vscode.commands.registerTextEditorCommand('intrexx-js-lib.start', (editor, edit) => {
let text = "FooBar";
edit.replace(editor.selection, text);
});
For use with multiple cursors, you'll have to iterate over editor.selections:
vscode.commands.registerTextEditorCommand('intrexx-js-lib.start', (editor, edit) => {
editor.selections.forEach((selection, i) => {
let text = "FooBar " + i
edit.replace(selection, text)
})
});

Get vscode registerCompletionItemProvider to work in a json file with a `word.` trigger

Update
Thanks to json: use default word pattern issue being resolved, vscode's json language server no longer includes the quotes around a word as part of the word, like "someWord" - now that word would be simply someWord.
In my case, as #rioV8 said, I was not explicitly setting the completionItem.range (because I was just going to use the default range). When you do not set your own range vscode uses the range of the word at the cursor in a completion - which used to include the " and that screwed up my completions.
The starting quote " is part of what VSCode considers the current
"word". Consequently, the completion items you return don't match the
current filter string " and are not displayed.
from Custom Extension for JSON Completion Does Not Work in Double Quotes
To fix that, all I needed to do was to explicity set the range like
item.range = new vscode.Range(position, position);
NOW after the linked fix, since the word no longer includes the " I do not (I tested it) need to explcitly set the range and the default range works fine.
I am using this code to try to register a CompletionProvider in my extension. It is essentially the code from the sample completionProvider sample https://github.com/microsoft/vscode-extension-samples/blob/master/completions-sample/src/extension.ts.
I want it triggered by a . as in "launches." in my extension command in keybindings.json ultimately but it is doing nothing in any json file. Nothing happens, no error.
function activate(context) {
loadLaunchSettings(context);
activeContext = context;
const configCompletionProvider = vscode.languages.registerCompletionItemProvider (
{ language: 'json', scheme: 'file' }, // tried scheme: 'untitled' too
{
// eslint-disable-next-line no-unused-vars
provideCompletionItems(document, position, token, context) {
// get all text until the `position` and check if it reads `"launches."`
const linePrefix = document.lineAt(position).text.substr(0, position.character);
if (!linePrefix.endsWith('\"launches.\"')) { // tried without the escapes too
return undefined;
}
return [
new vscode.CompletionItem('log', vscode.CompletionItemKind.Text),
new vscode.CompletionItem('warn', vscode.CompletionItemKind.Text),
new vscode.CompletionItem('error', vscode.CompletionItemKind.Text),
];
}
},
'.' // trigger
);
context.subscriptions.push(configCompletionProvider);
}
In this code:
{
"key": "alt+f",
"command": "launches." <= provider completion options here
},
I couldn't find anything helpful and thought I followed the sample closely but no completion suggestions either on typing "launches." or using Ctrl+Space to trigger intellisense.
I do have this setting:
"editor.quickSuggestions": {
"comments": true,
"other": true,
"strings": true // <===
},
And I tried various alternatives presented here to a similar problem: Custom Extension for JSON Completion Does Not Work in Double Quotes
Based on the answer by Gamma11 about what is a word in JSON, the whole string is considered a word including the " chars.
It works if you adjust the range the completion item should replace, and not look for the current word at the position.
context.subscriptions.push(vscode.languages.registerCompletionItemProvider (
{ language: 'json', scheme: 'file' },
// 'json',
{
// eslint-disable-next-line no-unused-vars
provideCompletionItems(document, position, token, context) {
// get all text until the `position` and check if it reads `"launches.`
const linePrefix = document.lineAt(position).text.substring(0, position.character);
if (!linePrefix.endsWith('"launches.')) {
return undefined;
}
let myitem = (text) => {
let item = new vscode.CompletionItem(text, vscode.CompletionItemKind.Text);
item.range = new vscode.Range(position, position);
return item;
}
return [
myitem('log'),
myitem('warn'),
myitem('error'),
];
}
},
'.' // trigger
));
Edit:
What also works but does not look nice is
return [
new vscode.CompletionItem('"launches.log"', vscode.CompletionItemKind.Text),
new vscode.CompletionItem('"launches.warn"', vscode.CompletionItemKind.Text),
new vscode.CompletionItem('"launches.error"', vscode.CompletionItemKind.Text),
];
Edit:
Just to supply a completion on any . typed I just removed the test (endsWith) of what was in front of the ..
To see if the completion provider is called I place a LogPoint breakpoint on the return with the CompletionItems.
The documentation of the CompletionItem is very terse.
From the doc of CompletionItem:
It is sufficient to create a completion item from just a label. In that case the completion item will replace the word until the cursor with the given label or insertText. Otherwise the given edit is used.
Although they talk about an edit in the main text, the textEdit doc tells you it is deprecated and you need to use insertText and range. But the additionalTextEdits are not deprecated (??)
The range property is not very clear how an inserting and replacing range are used and what effect you can achieve by setting it a certain way.
When omitted, the range of the current word is used as replace-range and as insert-range the start of the current word to the current position is used.
And then part of the problem is that " is part of a word for JSON files. And as Gamma11 has pointed out if you, for some odd reason, add these "'s to the label it works in some cases. Setting the insertText with the same content does not work, probably because the default range is chosen incorrectly.
If you set the range yourself you bypass the strange default behavior.
Because we want to insert new stuff at the position of the cursor just set range to an empty range at the cursor position.
context.subscriptions.push(vscode.languages.registerCompletionItemProvider (
// { language: 'json', scheme: 'file' },
'json',
{
// eslint-disable-next-line no-unused-vars
provideCompletionItems(document, position, token, context) {
let myitem = (text) => {
let item = new vscode.CompletionItem(text, vscode.CompletionItemKind.Text);
item.range = new vscode.Range(position, position);
return item;
}
return [
myitem('howdy1'),
myitem('howdy2'),
myitem('howdy3'),
];
}
},
'.' // trigger
));

Set cursor location in CompletionItem

I want to add numbers to the sass-indentation extension and I kind of figured out how to that, it would be nice if i could set the cursor location when the suggestion gets triggered, just like you can set the cursor location when you make snippets with $1, is that possible ?
import { CompletionItem, CompletionItemKind } from 'vscode';
const sassSchemaTest = [
{
name: '%',
body: '$1%', // I want the cursor location where the '$' sign is
description: 'test'
}
];
export default sassSchemaTest.map(item => {
const completionItem = new CompletionItem(item.name);
completionItem.insertText = item.body;
completionItem.detail = item.description;
completionItem.kind = CompletionItemKind.Property;
return completionItem;
});
Yes, completion items support the usual snippet syntax. Simply use a vscode.SnippetString for insertText instead of a raw string.
A string or snippet that should be inserted in a document when selecting this completion. When falsy the label is used.
completionItem.insertText = new vscode.SnippetString(item.body);

How do I add a "save" button to the gtk filechooser dialog?

I have a Gjs app that will need to save files. I can open the file chooser dialog just fine from my menu, and I have added a "save" and "cancel" button, but I can't get the "save" button to trigger anything.
I know I'm supposed to pass it a response_id, but I'm not sure what that's supposed to look like nor what I'm supposed to do with it afterwards.
I read that part here:
https://www.roojs.com/seed/gir-1.2-gtk-3.0/gjs/Gtk.FileChooserDialog.html#expand
let actionSaveAs = new Gio.SimpleAction ({ name: 'saveAs' });
actionSaveAs.connect('activate', () => {
const saver = new Gtk.FileChooserDialog({title:'Select a destination'});
saver.set_action(Gtk.FileChooserAction.SAVE);
saver.add_button('save', 'GTK_RESPONSE_ACCEPT');
saver.add_button('cancel', 'GTK_RESPONSE_CANCEL');
const res = saver.run();
if (res) {
print(res);
const filename = saver.get_filename();
print(filename);
}
saver.destroy();
});
APP.add_action(actionSaveAs);
I can catch res and fire the associated little logging action when I close the dialog, but both the "save" and "cancel" buttons just close the dialog without doing or saying anything.
My question is, what are GTK_RESPONSE_ACCEPT and GTK_RESPONSE_CANCEL supposed to be (look like) in GJS and how do I use them?
In GJS enums like GTK_RESPONSE_* are numbers and effectively look like this:
// imagine this is the Gtk import
const Gtk = {
ResponseType: {
NONE: -1,
REJECT: -2,
ACCEPT: -3,
DELETE_EVENT: -4,
...
}
};
// access like so
let response_id = -3;
if (response_id === Gtk.ResponseType.ACCEPT) {
log(true);
}
There's a bit more information here about that.
let saver = new Gtk.FileChooserDialog({
title:'Select a destination',
// you had the enum usage correct here
action: Gtk.FileChooserAction.SAVE
});
// Really the response code doesn't matter much, since you're
// deciding what to do with it. You could pass number literals
// like 1, 2 or 3. Probably this was not working because you were
// passing a string as a response id.
saver.add_button('Cancel', Gtk.ResponseType.CANCEL);
saver.add_button('Save', Gtk.ResponseType.OK);
// run() is handy, but be aware that it will block the current (only)
// thread until it returns, so I usually prefer to connect to the
// GtkDialog::response signal and use GtkWidget.show()
saver.connect('response', (dialog, response_id) => {
if (response_id === Gtk.ResponseType.OK) {
// outputs "-5"
print(response_id);
// NOTE: we're using #dialog instead of 'saver' in the callback to
// avoid a possible cyclic reference which could prevent the dialog
// from being garbage collected.
let filename = dialog.get_filename();
// here's where you do your stuff with the filename. You might consider
// wrapping this whole thing in a re-usable Promise. Then you could call
// `resolve(filename)` or maybe `resolve(null)` if the response_id
// was not Gtk.ResponseType.OK. You could then `await` the result to get
// the same functionality as run() but allow other code to execute while
// you wait for the user.
print(filename);
// Also note, you actually have to do the writing yourself, such as
// with a GFile. GtkFileChooserDialog is really just for getting a
// file path from the user
let file = Gio.File.new_for_path(filename);
file.replace_contents_bytes_async(
// of course you actually need bytes to write, since GActions
// have no way to return a value, unless you're passing all the
// data through as a parameter, it might not be the best option
new GLib.Bytes('file contents to write to disk'),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null,
// "shadowing" variable with the same name is another way
// to prevent cyclic references in callbacks.
(file, res) => {
try {
file.replace_contents_finish(res);
} catch (e) {
logError(e);
}
}
);
}
// destroy the dialog regardless of the response when we're done.
dialog.destroy();
});
// for bonus points, here's how you'd implement a simple preview widget ;)
saver.preview_widget = new Gtk.Image();
saver.preview_widget_active = false;
this.connect('update-preview', (dialog) => {
try {
// you'll have to import GdkPixbuf to use this
let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
dialog.get_preview_filename(),
dialog.get_scale_factor() * 128,
-1
);
dialog.preview_widget.pixbuf = pixbuf;
dialog.preview_widget.visible = true;
dialog.preview_widget_active = true;
// if there's some kind of error or the file isn't an image
// we'll just hide the preview widget
} catch (e) {
dialog.preview_widget.visible = false;
dialog.preview_widget_active = false;
}
});
// this is how we'll show the dialog to the user
saver.show();

How to remove selection after replace in vscode API

while creating an extension for vscode I got stuck in selection, now the problem is when I replace some range of textEditor through an api it replaces that range as well as make that range selected. For snippets this is a good idea but my extension requirement is not to select replaced text, I searched in api but did't find anything related to remove text selection (Selection occurs when the document is empty)
editor.edit((editBuilder)=>{ //editor is an object of active text editor
editBuilder.replace(textRange,text) // text = 'dummydummydummy'
}) //after this I got the following output
editor.edit(builder => {
builder.replace(selection, newStr);
})
// The edit call returns a promise. When that resolves you can set
// the selection otherwise you interfere with the edit itself.
// So use "then" to sure edit call is done;
.then(success => {
console.log("success:", success);
// Change the selection: start and end position of the new
// selection is same, so it is not to select replaced text;
var postion = editor.selection.end;
editor.selection = new vscode.Selection(postion, postion);
});
I believe that this is happening because the edit is being applied within the current selection. edit returns a promise that is resolved when the edit is applied, and you can use this to set the selection after the edit is successful:
editor.edit((editBuilder) => {
editBuilder.replace(textRange, text)
}).then(success => {
if (success) {
// make selection empty
editor.selection.active = editor.selection.anchor
}
})
let editor = vscode.window.activeTextEditor;
let selection = editor.selection;
editor.edit(builder => {
builder.replace(selection, newStr);
});
see: TextEditorEdit API Doc