diff --git a/Public/js/app.js b/Public/js/app.js index eaf85164..13f5ab7e 100644 --- a/Public/js/app.js +++ b/Public/js/app.js @@ -166,6 +166,9 @@ export class App { case "diagnostics": this.updateLanguageServerStatus(true); this.editor.clearMarkers(); + // Reset the fix-its surfaced as Quick Fix code actions; repopulated + // below from the new diagnostics (sourcekit-lsp ships them inline). + this.codeActionEntries = []; if (!response.value) { return; @@ -181,7 +184,7 @@ export class App { const startLineNumber = start.line + 1; const startColumn = start.character + 1; const endLineNumber = end.line + 1; - const endColumn = start.character + 1; + const endColumn = end.character + 1; let severity = languageServer.convertDiagnosticSeverity( diagnostic.severity @@ -194,10 +197,23 @@ export class App { endColumn: endColumn, message: diagnostic.message, severity: severity, - source: diagnostic.source, + // Fall back to a stable source so LSP markers are always + // distinguishable from runner markers (which set no source) when + // matching fix-its below. + source: diagnostic.source ?? "sourcekit-lsp", }; }); + this.codeActionEntries = diagnostics + .filter((diagnostic) => diagnostic.codeActions?.length) + .map((diagnostic) => ({ + startLineNumber: diagnostic.range.start.line + 1, + startColumn: diagnostic.range.start.character + 1, + message: diagnostic.message, + source: diagnostic.source ?? "sourcekit-lsp", + actions: diagnostic.codeActions, + })); + this.editor.updateMarkers(markers); break; case "format": @@ -289,6 +305,70 @@ export class App { }); }; + // Quick Fix: turn the fix-its sourcekit-lsp ships on each diagnostic into + // Monaco code actions for the markers under the cursor. No round-trip — + // the data already arrived with the diagnostics notification. + this.editor.oncodeaction = (model, _range, context) => { + const entries = this.codeActionEntries || []; + const markers = (context && context.markers) || []; + + const toEdits = (lspAction) => { + const changes = lspAction.edit && lspAction.edit.changes; + if (!changes) { + return null; + } + const edits = []; + // The document URI is the server's temp path; apply every edit to the + // single local model regardless of the key. + for (const uri of Object.keys(changes)) { + for (const textEdit of changes[uri]) { + edits.push({ + resource: model.uri, + versionId: undefined, + textEdit: { + range: { + startLineNumber: textEdit.range.start.line + 1, + startColumn: textEdit.range.start.character + 1, + endLineNumber: textEdit.range.end.line + 1, + endColumn: textEdit.range.end.character + 1, + }, + text: textEdit.newText, + }, + }); + } + } + return edits.length ? edits : null; + }; + + const actions = []; + for (const marker of markers) { + const entry = entries.find( + (e) => + e.source === marker.source && + e.startLineNumber === marker.startLineNumber && + e.startColumn === marker.startColumn && + e.message === marker.message + ); + if (!entry) { + continue; + } + for (const lspAction of entry.actions) { + const edits = toEdits(lspAction); + if (!edits) { + continue; + } + actions.push({ + title: lspAction.title, + kind: lspAction.kind || "quickfix", + diagnostics: [marker], + edit: { edits: edits }, + isPreferred: lspAction.isPreferred, + }); + } + } + return { actions: actions, dispose: () => {} }; + }; + this.editor.focus(); this.editor.scrollToBottm(); diff --git a/Public/js/editor.js b/Public/js/editor.js index b9163466..48e03f8b 100644 --- a/Public/js/editor.js +++ b/Public/js/editor.js @@ -55,6 +55,14 @@ export class Editor { }, }); + // Quick Fix (lightbulb) for diagnostics. sourcekit-lsp ships the fix-its + // inline on each diagnostic, so this just surfaces them as code actions. + monaco.languages.registerCodeActionProvider("swift", { + provideCodeActions: (model, range, context) => { + return this.oncodeaction(model, range, context); + }, + }); + // Explicit Ctrl+Space to trigger completion, like Xcode. (Cmd+Space is // taken by Spotlight on macOS, so bind WinCtrl = the Control key.) this.editor.addAction({ @@ -70,6 +78,7 @@ export class Editor { this.onhover = () => {}; this.oncompletion = () => {}; this.onsignaturehelp = () => {}; + this.oncodeaction = () => ({ actions: [], dispose() {} }); this.onaction = () => {}; }