Search code examples
typescriptvisual-studio-codevscode-extensionslanguage-server-protocol

How to create and edit a new file in the workspace via Language Server Extension


How do I get a Language Server Extension to trigger the creation of a new file , edit it, and display it in the attached client's workspace?

I have a LSP extension written with vscode-languageserver in node.js that executes a Command on the server via onExecuteCommand. I want this server-side command to trigger the client to create a new text file, populate it with some text, so it appears on the client's workspace list of open files.

Looking at https://github.com/microsoft/vscode-languageserver-node/blob/master/client-tests/src/helpers.test.ts I believe what I need to do is create a WorkspaceChange object, run createFile(), apply some changes (.insert) then tell the client to apply the edits via connection.workspace.applyEdit() but this does not work - no file is created and no errors are thrown in debugger.

Here is my code inside my server's onExecuteCommand:

//add some text
const textToAdd: string = "test string";

//create new WorkspaceChange obj
let workspaceChange = new WorkspaceChange();

//uri of the file we want to create
let newuri = 'file:///c:/temp/create.txt';

//make a TextEditChange obj. Fails if you do not supply version
let change = workspaceChange.getTextEditChange({ uri: newuri, version: 10 });

// give it some text 
change.insert(Position.create(0, 1), textToAdd);

// add a createFile documentChange to the workspaceChange
workspaceChange.createFile(newuri);

// pass these edits to the client to apply:
let reply = connection.workspace.applyEdit(workspaceChange.edit);
console.log(reply); //always <Pending>

If I supply a non-existent file name, then the process fails - no file is created or opened in the workspace.

However if I supply an existing filename, the edits are applied and the file is opened in the workspace as expected.

I thought it was because I was supplying an edit prior to a createFile, but if I run getTextEditChange() before createFile() then the process fails with error "Workspace edit is not configured for document changes"


Solution

  • Thanks for the kick, yes I got this working some time later

    It is implemented here: https://github.com/proclaimforum/vscode-proclaimscript-language/blob/master/server/src/server.ts

    Lines 476 onwards are of note, where:

    1. a CreateFile variable is constructed, holding the path (uri) of the file to create, and formed into an array of CreateFiles (lines 476-482)
    2. a WorkspaceEdit variable is created, specifying our above CreateFile array for the documentChanges property (485)
    3. we pass this to the client to apply via workspace.applyEdit (488). This actually creates the file.
    4. Text to be added to the new document is first formed into an array of TextEdit objects, consisting of a range (range:) and content (newText:) (491-498)
    5. an array of TextDocumentEdit is constructed, containing our TextEdit (500-503)
    6. we update our workspaceEdit variable property to reference to this array of TextDocumentEdits (506)
    7. and finally ask our connected client to apply the edits (510)

    I copy the relevant code section below for reference, but have a look at the github link for full implementation, including setting up the client connection etc, setting capabilities etc.

    //uri of new file
    let currentPath :string = (thisdoc.uri).substr(0,thisdoc.uri.lastIndexOf('/'));
    
    let newuri = currentPath+'/syntaxcheck.txt';
    
    //construct a CreateFile variable
    let createFile: CreateFile = { kind: 'create', uri: newuri };
    //and make into array
    let createFiles: CreateFile[] = [];
    createFiles.push(createFile);
    
    //make a new workspaceEdit variable, specifying a createFile document change
    var workspaceEdit: WorkspaceEdit = { documentChanges: createFiles };
    
    //pass to client to apply this edit
    await connection.workspace.applyEdit(workspaceEdit);
    
    
    //To insert the text (and pop up the window), create array of TextEdit
    let textEdit: TextEdit[] = [];
    //let document = documents.get(newuri);
    let documentRange: Range = Range.create(0, 0, Number.MAX_VALUE, Number.MAX_VALUE);
    //populate with the text, and where to insert (surely this is what workspaceChange.insert is for?)
    let textEdits: TextEdit = { range: documentRange, newText: syntaxmessage };
    
    textEdit.push(textEdits);
    
    //make a new array of textDocumentEdits, containing our TextEdit (range and text)
    let textDocumentEdit = TextDocumentEdit.create({ uri: newuri, version: 1 }, textEdit);
    let textDocumentEdits: TextDocumentEdit[] = [];
    textDocumentEdits.push(textDocumentEdit);
    
    //set  our workspaceEdit variable to this new TextDocumentEdit
    workspaceEdit = { documentChanges: textDocumentEdits };
    
    //and finally apply this to our workspace.
    // we can probably do this some more elegant way 
    connection.workspace.applyEdit(workspaceEdit);