[add] diags from server

This commit is contained in:
Andy Bunce 2025-08-14 18:20:41 +01:00
parent 5c759eb3f4
commit 2078055d05
8 changed files with 150 additions and 15 deletions

View file

@ -1,6 +1,6 @@
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { openSearchPanel, highlightSelectionMatches,searchKeymap } from '@codemirror/search'; import { openSearchPanel, highlightSelectionMatches,searchKeymap } from '@codemirror/search';
import {openLintPanel, lintGutter, lintKeymap} from "@codemirror/lint" import {openLintPanel, lintGutter, lintKeymap,linter,setDiagnostics} from "@codemirror/lint"
import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands'; import { indentWithTab, history, defaultKeymap, historyKeymap } from '@codemirror/commands';
import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; import { foldGutter, indentOnInput, indentUnit, bracketMatching, foldKeymap, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'; import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
@ -8,7 +8,7 @@ import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSele
import {markdown} from "@codemirror/lang-markdown"; import {markdown} from "@codemirror/lang-markdown";
// Theme // Theme
import { oneDark } from "@codemirror/theme-one-dark"; import { oneDark } from "@codemirror/theme-one-dark";
import { LSPClient, languageServerSupport } from "@codemirror/lsp-client"; import { LSPClient, LSPPlugin,languageServerSupport } from "@codemirror/lsp-client";
// Language // Language
import { xml } from "@codemirror/lang-xml"; import { xml } from "@codemirror/lang-xml";
@ -81,4 +81,5 @@ function createEditorView(state, parent) {
return new EditorView({ state, parent }); return new EditorView({ state, parent });
} }
export { createEditorState, createEditorView, openSearchPanel,openLintPanel, languageServerSupport, baseExts ,client, simpleWebSocketTransport};
export { createEditorState, createEditorView, openSearchPanel,openLintPanel, languageServerSupport, baseExts ,client, simpleWebSocketTransport,linter, LSPPlugin,setDiagnostics};

View file

@ -26,9 +26,8 @@ declare %unit:test function test:resolvePosition() {
(:~ convert all position. :) (:~ convert all position. :)
declare %unit:test function test:toPosition() { declare %unit:test function test:toPosition() {
let $text:=unparsed-text("sample.docs/pdfbox.xqm") let $text:=unparsed-text("sample.docs/pdfbox.xqm")
let $nl:= index-of(string-to-codepoints($text),10)
return for $i in 0 to string-length($text)-1 return for $i in 0 to string-length($text)-1
let $pos:=pos:toPosition($text,$i,$nl) let $pos:=pos:toPosition($text,$i)
return unit:assert-equals($i,pos:resolvePosition($text,$pos)) return unit:assert-equals($i,pos:resolvePosition($text,$pos))
}; };

View file

@ -25,7 +25,7 @@ declare function lsp-diags:publish(
$xml as lsp-diags:ParseResult) $xml as lsp-diags:ParseResult)
as map(*){ as map(*){
let $diagnostics:=if($xml/self::ERROR) let $diagnostics:=if($xml/self::ERROR)
then [lsp-diags:parse-error($text, $xml)] then array{lsp-diags:parse-error($text, $xml)}
else [] else []
return {"jsonrpc": "2.0", return {"jsonrpc": "2.0",
@ -38,9 +38,13 @@ as map(*){
<ERROR b="10819" e="10820" o="234" s="43">syntax error, found '}' while expecting [S,'else'] at line 290, column 3: ...} }; ? return bookmark info for children of $outlineItem as s...</ERROR> <ERROR b="10819" e="10820" o="234" s="43">syntax error, found '}' while expecting [S,'else'] at line 290, column 3: ...} }; ? return bookmark info for children of $outlineItem as s...</ERROR>
:) :)
declare function lsp-diags:parse-error($text as xs:string, $xml as element(ERROR)) declare function lsp-diags:parse-error($text as xs:string, $xml as element(ERROR))
as map(*)?{ as map(*)*{
lsp-diags:nostic(pos:Range(pos:toPosition($text=>trace("EXML "), $xml/@b), lsp-diags:nostic(pos:Range(pos:toPosition($text=>trace("EXML "), $xml/@b),
pos:toPosition($text, $xml/@e)), pos:toPosition($text, $xml/@e)),
1,$xml/string())=>trace("EEEE ") 1,$xml/string())=>trace("EEEE "),
lsp-diags:nostic(pos:Range(pos:toPosition($text=>trace("EXML "), $xml/@e +1 ),
pos:toPosition($text, string-length($text)-1)),
1,"Previous parse error")
}; };

View file

@ -3,13 +3,15 @@ text editing API
:) :)
module namespace pos="lsp/position"; module namespace pos="lsp/position";
declare type pos:num as (xs:integer|xs:double);
(:~ (:~
@param line Line position in a document (zero-based). @param line Line position in a document (zero-based).
@param character Character offset on a line in a document (zero-based). @param character Character offset on a line in a document (zero-based).
:) :)
declare record pos:Position( declare record pos:Position(
line as xs:integer, line as pos:num,
character as xs:integer character as pos:num
); );
(:~ (:~
@ -38,7 +40,7 @@ as xs:string{
(:~ find index from Position :) (:~ find index from Position :)
declare function pos:resolvePosition($text as xs:string, $pos as pos:Position) declare function pos:resolvePosition($text as xs:string, $pos as pos:Position)
as xs:integer as pos:num
{ {
let $nl:= index-of(string-to-codepoints($text),10) let $nl:= index-of(string-to-codepoints($text),10)
let $off:=if($pos?line eq 0) let $off:=if($pos?line eq 0)
@ -49,7 +51,7 @@ as xs:integer
(:~ convert index into Position :) (:~ convert index into Position :)
declare function pos:toPosition($text as xs:string, declare function pos:toPosition($text as xs:string,
$index as xs:integer $index as pos:num
) )
as pos:Position { as pos:Position {
let $nl:= index-of(string-to-codepoints($text),10) let $nl:= index-of(string-to-codepoints($text),10)
@ -63,7 +65,7 @@ return pos:Position($line, $index - $off)
(:~ line number for $pos :) (:~ line number for $pos :)
declare function pos:lineAt($nl as xs:integer*,$pos as xs:integer) declare function pos:lineAt($nl as xs:integer*,$pos as pos:num)
as xs:integer as xs:integer
{ {
if($pos le head($nl)) if($pos le head($nl))

View file

@ -16730,6 +16730,16 @@ var lsp = (function (exports) {
return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of(lintExtensions)); return state.field(lintState, false) ? effects : effects.concat(StateEffect.appendConfig.of(lintExtensions));
} }
/** /**
Returns a transaction spec which updates the current set of
diagnostics, and enables the lint extension if if wasn't already
active.
*/
function setDiagnostics(state, diagnostics) {
return {
effects: maybeEnableLint(state, [setDiagnosticsEffect.of(diagnostics)])
};
}
/**
The state effect that updates the set of active diagnostics. Can The state effect that updates the set of active diagnostics. Can
be useful when writing an extension that needs to track these. be useful when writing an extension that needs to track these.
*/ */
@ -16846,6 +16856,65 @@ var lsp = (function (exports) {
{ key: "Mod-Shift-m", run: openLintPanel, preventDefault: true }, { key: "Mod-Shift-m", run: openLintPanel, preventDefault: true },
{ key: "F8", run: nextDiagnostic } { key: "F8", run: nextDiagnostic }
]; ];
const lintPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
constructor(view) {
this.view = view;
this.timeout = -1;
this.set = true;
let { delay } = view.state.facet(lintConfig);
this.lintTime = Date.now() + delay;
this.run = this.run.bind(this);
this.timeout = setTimeout(this.run, delay);
}
run() {
clearTimeout(this.timeout);
let now = Date.now();
if (now < this.lintTime - 10) {
this.timeout = setTimeout(this.run, this.lintTime - now);
}
else {
this.set = false;
let { state } = this.view, { sources } = state.facet(lintConfig);
if (sources.length)
batchResults(sources.map(s => Promise.resolve(s(this.view))), annotations => {
if (this.view.state.doc == state.doc)
this.view.dispatch(setDiagnostics(this.view.state, annotations.reduce((a, b) => a.concat(b))));
}, error => { logException(this.view.state, error); });
}
}
update(update) {
let config = update.state.facet(lintConfig);
if (update.docChanged || config != update.startState.facet(lintConfig) ||
config.needsRefresh && config.needsRefresh(update)) {
this.lintTime = Date.now() + config.delay;
if (!this.set) {
this.set = true;
this.timeout = setTimeout(this.run, config.delay);
}
}
}
force() {
if (this.set) {
this.lintTime = Date.now();
this.run();
}
}
destroy() {
clearTimeout(this.timeout);
}
});
function batchResults(promises, sink, error) {
let collected = [], timeout = -1;
for (let p of promises)
p.then(value => {
collected.push(value);
clearTimeout(timeout);
if (collected.length == promises.length)
sink(collected);
else
timeout = setTimeout(() => sink(collected), 200);
}, error);
}
const lintConfig = /*@__PURE__*/Facet.define({ const lintConfig = /*@__PURE__*/Facet.define({
combine(input) { combine(input) {
return Object.assign({ sources: input.map(i => i.source).filter(x => x != null) }, combineConfig(input.map(i => i.config), { return Object.assign({ sources: input.map(i => i.source).filter(x => x != null) }, combineConfig(input.map(i => i.config), {
@ -16859,6 +16928,19 @@ var lsp = (function (exports) {
})); }));
} }
}); });
/**
Given a diagnostic source, this function returns an extension that
enables linting with that source. It will be called whenever the
editor is idle (after its content changed). If `null` is given as
source, this only configures the lint extension.
*/
function linter(source, config = {}) {
return [
lintConfig.of({ source, config }),
lintPlugin,
lintExtensions
];
}
function assignKeys(actions) { function assignKeys(actions) {
let assigned = []; let assigned = [];
if (actions) if (actions)
@ -31357,13 +31439,16 @@ ${text}</tr>
return new EditorView({ state, parent }); return new EditorView({ state, parent });
} }
exports.LSPPlugin = LSPPlugin;
exports.baseExts = baseExts; exports.baseExts = baseExts;
exports.client = client; exports.client = client;
exports.createEditorState = createEditorState; exports.createEditorState = createEditorState;
exports.createEditorView = createEditorView; exports.createEditorView = createEditorView;
exports.languageServerSupport = languageServerSupport; exports.languageServerSupport = languageServerSupport;
exports.linter = linter;
exports.openLintPanel = openLintPanel; exports.openLintPanel = openLintPanel;
exports.openSearchPanel = openSearchPanel; exports.openSearchPanel = openSearchPanel;
exports.setDiagnostics = setDiagnostics;
exports.simpleWebSocketTransport = simpleWebSocketTransport; exports.simpleWebSocketTransport = simpleWebSocketTransport;
return exports; return exports;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -65,6 +65,7 @@ function connect() {
const file = document.getElementById("iFile").value; const file = document.getElementById("iFile").value;
lsp.simpleWebSocketTransport(server) lsp.simpleWebSocketTransport(server)
.then(transport => { .then(transport => {
transport.subscribe(incoming);
client = lsp.client(transport); client = lsp.client(transport);
let plugin=lsp.languageServerSupport(client,file,"xquery") let plugin=lsp.languageServerSupport(client,file,"xquery")
const doc = view.state.doc.toString(); const doc = view.state.doc.toString();
@ -73,3 +74,46 @@ function connect() {
}) })
.catch(r => alert("connection failed: "+ server)); .catch(r => alert("connection failed: "+ server));
}; };
function incoming(msg) {
const rpc=JSON.parse(msg)
switch (rpc.method) {
case "textDocument/publishDiagnostics":
diags(rpc.params);
break;
default:
return;
}
};
function diags(params){
console.log("--",params)
let plugin= lsp.LSPPlugin.get(view)
const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({
from: plugin.fromPosition( range.start,view.state.doc),
to: plugin.fromPosition( range.end,view.state.doc),
severity: 'error',
message,
}))
.filter(
({ from, to }) =>
from !== null &&
to !== null &&
from !== undefined &&
to !== undefined,
)
.sort((a, b) => {
switch (true) {
case a.from < b.from:
return -1;
case a.from > b.from:
return 1;
}
return 0;
});
view.dispatch(lsp.setDiagnostics(view.state, diagnostics));
};