I have created a simple html page with Codemirror6 that talks via a websocket to a LSP server that I am developing.
I have notice that as I type didChange messages are sent, as I would expect.
Except no messages are sent when I use backspace,delete,return or paste in text.
Not author, but I have looked into this behavior before. Contrary to most language clients like VS Code, the changes are sent lazily. That is, the didChanges emit only when there is a request about to happen. When you insert characters, the completions under current cursor positions are continually requested, so you see didChange, but it is not the case when you delete characters.
Ok, but it seems odd to me. As a consumer of didChange messages I want to get one whenever the text has changed (maybe debounced) . Any change to the text may change the diagnostics etc. Why should the lsp-client have, for instance, different behaviour for a typed “A” against a pasted “A”?
They are sent on-demand when sync is called. So the reason you’re seeing different behavior for typing and pasting is that one triggers completion (which will sync) and the other does not. You can run your own code to call LSPClient.sync when the document has changed, with a debounce.
Ok, My focus at the moment is on displaying diagnostics. My server sends them on recieving didOpen/didChange. I have added a “sync” button to my UI client.sync();. This works but I want it to be automatic.
The Lint plugin has the linterfunction that appears to be called when I should do the sync but it assumes the diagnostics (or a Promise of diagnostics) are available then, but that is not my case.
I want a simple way to call client.sync() …whenever the editor is idle (after its content changed).
What is the best approach?
Sort of works, but I get a flicker as the old diagnostics are removed then later updated. And it seems like unnessary work. Perhaps the linter function could return a special value to indicate no new diagnostics are currently available?
I find that the linter package can actually achieve this through the forceLinting function. My approach is to trigger reevaluation on our LintSource instance when receiving diagnostics. If you sync the document in that function, it creates an evaluation loop. The loop iterates until the user stop to update the document, and the server stops to publish new diagnostics. At this moment, the state settles.
// state field storing the linter state
const lspPublishedDiagnostics = StateField.define<Diagnostic[]>({
create() { return [] },
update(value, tr) { /* ... reduce through setPublishedDiagnostics ... */ },
})
// the state effect updating lspPublishedDiagnostics
const setPublishedDiagnostics = StateEffect.define<Diagnostic[]>()
export function lspLinter(): LSPClientExtension {
return {
clientCapabilities: { /* ... */ },
notificationHandlers: {
'textDocument/publishDiagnostics': (client, params: lsp.PublishDiagnosticsParams) => {
/* ... get the view and the plugin ... */
view.dispatch({
effects: storeLspDiagnostics(plugin, params.diagnostics),
})
/* trigger linting update, utilizing needsRefresh (see below) */
forceLinting(view)
return true
}
},
}
}
Synchronize the document whenever LintSource is recomputed.
const lspLinterSource: LintSource = view => {
const plugin = LSPPlugin.get(view)
if (!plugin) return []
return getDiagnostics(plugin, view.state)
}
async function getDiagnostics(plugin: LSPPlugin, state: EditorState) {
/* ... get plugin ... */
plugin.client.sync()
/* ... query lspPublishedDiagnostics state field ... */
}
Include this effect in the needsRefresh function to force the forceLinting function to always invalidate the previous diagnostics.
@qbane, Thanks for sharing this. My solution above has timing issues, as well as the listed problems. I have tried a couple of improved solutions, but I am hopeful that @codemirror/lsp-client - #11 by marijn will allow me to throw them away.