# HG changeset patch # User wenzelm # Date 1761228940 -7200 # Node ID 486e094b676c2e9d82499d636431284bdd7c955b # Parent 3de18e94ac7c94981632e764dbfb3686ada1d7b3 various improvements of Isabelle/VSCode: original changeset by Diana Korchmar, LMU München; diff -r 3de18e94ac7c -r 486e094b676c etc/build.props --- a/etc/build.props Thu Oct 23 14:49:21 2025 +0200 +++ b/etc/build.props Thu Oct 23 16:15:40 2025 +0200 @@ -283,6 +283,7 @@ src/Tools/VSCode/src/language_server.scala \ src/Tools/VSCode/src/lsp.scala \ src/Tools/VSCode/src/pretty_text_panel.scala \ + src/Tools/VSCode/src/sledgehammer_panel.scala \ src/Tools/VSCode/src/preview_panel.scala \ src/Tools/VSCode/src/state_panel.scala \ src/Tools/VSCode/src/vscode_main.scala \ diff -r 3de18e94ac7c -r 486e094b676c src/Pure/PIDE/query_operation.scala --- a/src/Pure/PIDE/query_operation.scala Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Pure/PIDE/query_operation.scala Thu Oct 23 16:15:40 2025 +0200 @@ -166,13 +166,13 @@ editor.require_dispatcher { editor.session.cancel_exec(current_state.value.exec_id) } def apply_query(query: List[String]): Unit = { - editor.require_dispatcher {} + editor.send_dispatcher { - editor.current_node_snapshot(editor_context) match { - case Some(snapshot) => - remove_overlay() - current_state.change(_ => Query_Operation.State.empty) - consume_output(Document.Snapshot.init, Command.Results.empty, Nil) + editor.current_node_snapshot(editor_context) match { + case Some(snapshot) => + remove_overlay() + current_state.change(_ => Query_Operation.State.empty) + consume_output(Document.Snapshot.init, Command.Results.empty, Nil) editor.current_command(editor_context, snapshot) match { case Some(command) => @@ -182,9 +182,10 @@ case None => } - consume_status(current_state.value.status) - editor.flush() - case None => + consume_status(current_state.value.status) + editor.flush() + case None => + } } } diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/etc/options --- a/src/Tools/VSCode/etc/options Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/etc/options Thu Oct 23 16:15:40 2025 +0200 @@ -3,6 +3,9 @@ option vscode_input_delay : real = 0.1 for vscode -- "delay for client input (edits)" +option sledgehammer_provers_history : string = "" for vscode + -- "history of used Sledgehammer provers" + option vscode_output_delay : real = 0.5 for vscode -- "delay for client output (rendering)" diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/MANIFEST --- a/src/Tools/VSCode/extension/MANIFEST Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/extension/MANIFEST Thu Oct 23 16:15:40 2025 +0200 @@ -5,15 +5,26 @@ isabelle_vscode.png media/main.js media/Preview_inverse.svg +media/documentation-panel.svg +media/symbol-panel.svg +media/sledgehammer-panel.svg +media/output-panel.svg media/PreviewOnRightPane_16x_dark.svg media/PreviewOnRightPane_16x.svg media/Preview.svg media/ViewSource_inverse.svg media/ViewSource.svg media/vscode.css +media/symbols.css +media/symbols.js +media/documentation.css +media/documentation.js +media/sledgehammer.css +media/sledgehammer.js package.json README.md src/decorations.ts +src/documentation_panel.ts src/extension.ts src/file.ts src/library.ts @@ -22,7 +33,9 @@ src/platform.ts src/preview_panel.ts src/script_decorations.ts +src/sledgehammer_panel.ts src/state_panel.ts +src/symbol_panel.ts src/vscode_lib.ts test/extension.test.ts test/index.ts diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/documentation-panel.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/documentation-panel.svg Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,1 @@ + \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/documentation.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/documentation.css Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,66 @@ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +--font-size-base: var(--vscode-editor-font-size); +--font-size-small: calc(var(--font-size-base) * 0.85); + +body { + font-family: var(--vscode-editor-font-family); + font-size: var(--font-size-base); + color: var(--vscode-foreground); + background-color: var(--vscode-sideBar-background); + padding: var(--container-paddding); + margin: 0; +} + +.doc-list { + list-style-type: none; + padding-left: 0; +} + +.doc-section { + font-weight: normal; +} +.section-title { + font-weight: normal; + color: var(--vscode-foreground); +} + +.doc-sublist { + list-style-type: none; + padding-left: 10px; +} + +.doc-link { + color: var(--vscode-textLink-foreground); + text-decoration: none; + outline: none; +} + +.doc-link:focus { + outline: none; +} + +.doc-entry { + margin: 2px 0; + cursor: pointer; +} + +.doc-entry.selected { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} + +.doc-entry.selected .doc-link { + color: var(--vscode-list-activeSelectionForeground); +} + +.doc-entry:not(.selected) .doc-link:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); +} diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/documentation.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/documentation.js Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,105 @@ +(function () { + const vscode = acquireVsCodeApi(); + + window.addEventListener("message", event => { + const message = event.data; + + if (message.command === "update" && Array.isArray(message.sections)) { + renderDocumentation(message.sections); + } + }); + + function renderDocumentation(sections) { + const container = document.getElementById("documentation-container"); + if (!container) return; + container.innerHTML = ""; + + vscode.setState({ sections: sections }); + + const list = document.createElement("ul"); + list.classList.add("doc-list"); + + let selectedEntry = null; + + sections.forEach(section => { + const sectionItem = document.createElement("li"); + sectionItem.classList.add("doc-section"); + + const titleElement = document.createElement("span"); + titleElement.textContent = section.title; + titleElement.classList.add("section-title"); + + const subList = document.createElement("ul"); + subList.classList.add("doc-sublist"); + subList.style.display = section.important ? "block" : "none"; + + titleElement.addEventListener("click", () => { + subList.style.display = subList.style.display === "none" ? "block" : "none"; + }); + + section.entries.forEach(entry => { + const entryItem = document.createElement("li"); + entryItem.classList.add("doc-entry"); + + let cleanedPath = entry.path.replace(/["]/g, ""); + if (section.title.includes("Examples")) { + cleanedPath = cleanedPath.replace(/^.*?src\//, "src/"); + } + if (section.title.includes("Release Notes")) { + cleanedPath = cleanedPath.replace(/^\$ISABELLE_HOME\//, ""); + } + + let displayText = cleanedPath; + const match = cleanedPath.match(/([^\/]+?)(?:\.pdf)?$/); + if (match) { + const filename = match[1]; + const cleanTitle = entry.title.replace(/["']/g, "").trim(); + displayText = `${filename}${cleanTitle ? ': ' + cleanTitle : ''}`; + } + + const entryLink = document.createElement("a"); + entryLink.innerHTML = displayText; + entryLink.href = "#"; + entryLink.classList.add("doc-link"); + + entryLink.addEventListener("click", event => { + event.preventDefault(); + + if (selectedEntry && selectedEntry !== entryItem) { + selectedEntry.classList.remove("selected"); + } + + if (selectedEntry === entryItem) { + selectedEntry.classList.remove("selected"); + selectedEntry = null; + } else { + selectedEntry = entryItem; + entryItem.classList.add("selected"); + openFile(entry.path); + } + }); + + entryItem.appendChild(entryLink); + subList.appendChild(entryItem); + }); + + sectionItem.appendChild(titleElement); + sectionItem.appendChild(subList); + list.appendChild(sectionItem); + }); + + container.appendChild(list); + } + + function openFile(filePath) { + vscode.postMessage({ command: "openFile", path: filePath }); + } + + window.onload = function () { + const savedState = vscode.getState(); + if (savedState && savedState.sections) { + renderDocumentation(savedState.sections); + } + }; + })(); + \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/output-panel.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/output-panel.svg Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,1 @@ + \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/sledgehammer-panel.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/sledgehammer-panel.svg Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,1 @@ + \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/sledgehammer.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/sledgehammer.css Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,300 @@ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +--font-size-base: var(--vscode-editor-font-size); +--font-size-small: calc(var(--font-size-base) * 0.85); +--font-size-tiny: calc(var(--font-size-base) * 0.75); + +body { + padding: 0 var(--container-paddding); + font-size: var(--vscode-editor-font-size); + font-weight: var(--vscode-editor-font-weight); + font-family: var(--vscode-editor-font-family); + color: var(--vscode-foreground); + background-color: var(--vscode-sideBar-background); +} + +#sledgehammer-container { + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + padding: 6px; + color: var(--vscode-foreground); +} + +#sledgehammer-container .top-row { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + column-gap: 4px; + row-gap: 2px; + margin-bottom: 4px; +} + +#sledgehammer-container label { + display: flex; + align-items: center; + gap: 2px; + margin: 2px 0; +} + +#sledgehammer-spinner { + width: 10px; + height: 10px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-top-color: #000; + border-radius: 50%; + animation: spin 1s linear infinite; + display: none; +} + +#sledgehammer-spinner.loading { + display: inline-block; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +#sledgehammer-container input[type="text"] { + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + width: 23em; + min-height: calc(var(--font-size-base) * 1.4); + box-sizing: border-box; + margin-right: 4px; + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); +} + +#sledgehammer-container input[type="text"]:focus { + outline: 1.5px solid #9dc3e1; +} + +#sledgehammer-container input[type="checkbox"] { + margin: 0; + transform: translateY(0.5px); +} + +#sledgehammer-container button { + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + font-weight: normal; + border: 0.5px solid #e2e2e2; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + cursor: pointer; + margin: 2px 0; +} + +#sledgehammer-container button#apply-btn { + font-weight: bold; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +#sledgehammer-container button:hover { + background-color: var(--vscode-button-hoverBackground); + border: 0.5px solid #9dc3e1; +} + +#result button { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: none; + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + cursor: pointer; +} + +#result button:hover { + background-color: var(--vscode-button-hoverBackground); + color: var(--vscode-button-foreground); +} + +#sledgehammer-container select { + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + height: 10px; + margin: 2px 0; + background-color: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); +} + +#status { + font-style: italic; + color: var(--vscode-descriptionForeground); +} + +#result div { + padding: 2px 4px; + border-bottom: 1px solid #ffffff; + white-space: pre-wrap; + background-color: var(--vscode-editor-inactiveSelectionBackground); + color: var(--vscode-foreground); + margin-bottom: 1px; +} + +#result .sledge-line { + background-color: var(--vscode-editor-inactiveSelectionBackground); + border-bottom: 1px solid var(--vscode-panel-border); + color: #000; + white-space: pre-wrap; + padding: 2px 6px; + margin-bottom: 1px; + border-radius: 2px; +} + +#result .interrupt { + background-color: var(--vscode-editorError-background); + border-bottom: 1px solid #ffffff; + color: var(--vscode-editorError-foreground); + white-space: pre-wrap; + padding: 2px 6px; + margin-bottom: 1px; + border-radius: 2px; +} + +#result.error div { + background-color: #fdd; + color: var(--vscode-editorError-foreground); +} + +#sledgehammer-container .provers-dropdown .history-header { + font-family: var(--vscode-editor-font-family); + font-size: var(--font-size-small); + color: var(--vscode-foreground); + background: var(--vscode-dropdown-background); + cursor: default; + padding: 4px 8px; + border-bottom: 1px solid #f4f4f4; +} + +#sledgehammer-container .provers-dropdown .history-header:hover { + background: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); +} + +#sledgehammer-container .provers-dropdown div { + padding: 4px 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #f4f4f4; + font-size: var(--font-size-small); + background: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); +} + +#sledgehammer-container .provers-dropdown div:last-child { + border-bottom: none; +} + +#sledgehammer-container .provers-dropdown div:hover { + background: #307dc3; + color: var(--vscode-list-hoverForeground); +} + +#sledgehammer-container .provers-dropdown .delete-btn { + color: var(--vscode-errorForeground); + font-size: var(--font-size-small); + margin-left: 10px; + cursor: pointer; + user-select: none; + background: none; + border: none; + padding: 0 2px; + outline: none; +} + +#sledgehammer-container .provers-dropdown .delete-btn:hover { + background: var(--vscode-list-hoverBackground); + border-radius: 2px; +} + +.provers-dropdown { + position: absolute; + top: 110%; + left: 0; + width: 99%; + min-width: 99%; + box-sizing: border-box; + background: var(--vscode-dropdown-background); + border: 1px solid #e2e2e2; + border-top: none; + z-index: 20; + max-height: 200px; + overflow-y: auto; + font-family: var(--vscode-editor-font-family); + font-size: var(--font-size-small); + border-radius: 0 0 2px 2px; + display: none; +} + +.provers-input-wrapper { + position: relative; + display: flex; + align-items: center; + width: 23em; +} + +#provers { + width: 95%; + padding-right: 22px; +} + +.provers-dropdown-toggle { + position: absolute; + right: 3px; + top: 38%; + transform: translateY(-50%); + height: 16px; + width: 16px; + background: none !important; + border: none !important; + color: var(--vscode-foreground); + font-size: var(--font-size-small); + cursor: pointer; + z-index: 3; + display: flex; + align-items: center; + justify-content: center; + pointer-events: auto; + padding: 0; +} + +.provers-dropdown-toggle:hover, +.provers-dropdown-toggle:focus { + color: var(--vscode-textLink-foreground); + background: none; + border-radius: 0; +} + + +.provers-dropdown.visible { + display: block; +} + +.provers-dropdown .history-header { + font-family: var(--vscode-editor-font-family); + font-size: var(--font-size-small); + color: var(--vscode-dropdown-foreground); + background: var(--vscode-dropdown-background); + cursor: default; + padding: 4px 8px; + border-bottom: 1px solid #f4f4f4; +} + +.provers-dropdown div { + padding: 4px 8px; + cursor: pointer; + display: flex; +} \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/sledgehammer.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/sledgehammer.js Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,317 @@ +(function () { + const vscode = acquireVsCodeApi(); + let wasCancelled = false; + let kannbeCancelled = false; + + let history = []; + + const container = document.createElement('div'); + container.id = 'sledgehammer-container'; + + const topRow = document.createElement('div'); + topRow.classList.add('top-row'); + + const proversLabel = document.createElement('label'); + proversLabel.textContent = 'Provers: '; + + const proversInputWrapper = document.createElement('div'); + proversInputWrapper.className = 'provers-input-wrapper'; + + const proversInput = document.createElement('input'); + proversInput.type = 'text'; + proversInput.id = 'provers'; + proversInput.size = 30; + proversInput.value = ''; + proversInput.autocomplete = 'off'; + + proversInputWrapper.appendChild(proversInput); + + const chevronBtn = document.createElement('button'); + chevronBtn.type = 'button'; + chevronBtn.className = 'provers-dropdown-toggle'; + chevronBtn.setAttribute('aria-label', 'Show provers history'); + chevronBtn.tabIndex = -1; + chevronBtn.innerHTML = '▼'; + proversInputWrapper.appendChild(chevronBtn); + + proversLabel.appendChild(proversInputWrapper); + + const dropdown = document.createElement('div'); + dropdown.className = 'provers-dropdown'; + proversInputWrapper.appendChild(dropdown); + + function showDropdown() { + dropdown.classList.add('visible'); + } + function hideDropdown() { + dropdown.classList.remove('visible'); + } + + function renderDropdown(open = false) { + dropdown.innerHTML = ''; + const header = document.createElement('div'); + header.textContent = 'Previously entered strings:'; + header.className = 'history-header'; + dropdown.appendChild(header); + if (history.length === 0) { + const noEntry = document.createElement('div'); + noEntry.className = 'history-header'; + } else { + history.forEach(item => { + const row = document.createElement('div'); + row.tabIndex = 0; + row.textContent = item; + + const delBtn = document.createElement('span'); + delBtn.textContent = '✖'; + delBtn.className = 'delete-btn'; + delBtn.title = 'Delete entry'; + delBtn.addEventListener('mousedown', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + history = history.filter(h => h !== item); + renderDropdown(false); + showDropdown(); + + vscode.postMessage({ + command: 'delete_prover_history', + entry: item + }); + }); + + row.appendChild(delBtn); + row.addEventListener('mousedown', function (e) { + e.preventDefault(); + proversInput.value = item; + hideDropdown(); + proversInput.focus(); + }); + dropdown.appendChild(row); + }); + } + if (open) showDropdown(); else hideDropdown(); + } + + + chevronBtn.addEventListener('mousedown', function (e) { + e.preventDefault(); + if (dropdown.classList.contains('visible')) { + hideDropdown(); + } else { + renderDropdown(true); + showDropdown(); + proversInput.focus(); + } + }); + + proversInput.addEventListener('input', () => { }); + + proversInput.addEventListener('focus', function () { + renderDropdown(true); + showDropdown(); + }); + proversInput.addEventListener('blur', () => { + setTimeout(() => { + if (!dropdown.contains(document.activeElement)) hideDropdown(); + }, 150); + }); + + proversInput.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown' && dropdown.childNodes.length) { + dropdown.firstChild && dropdown.firstChild.focus && dropdown.firstChild.focus(); + } + }); + + function setHistory(newHistory) { + history = Array.isArray(newHistory) ? newHistory : []; + saveState(); + renderDropdown(false); + } + + function saveState() { + vscode.setState({ + history, + provers: proversInput.value, + isar: isarCheckbox.checked, + try0: try0Checkbox.checked + }); + } + + function addToHistory(entry) { + if (!entry.trim()) return; + history = [entry, ...history.filter((h) => h !== entry)].slice(0, 10); + saveState(); + renderDropdown(); + } + + const isarLabel = document.createElement('label'); + const isarCheckbox = document.createElement('input'); + isarCheckbox.type = 'checkbox'; + isarCheckbox.id = 'isar'; + isarLabel.appendChild(isarCheckbox); + isarLabel.appendChild(document.createTextNode(' Isar proofs')); + + const try0Label = document.createElement('label'); + const try0Checkbox = document.createElement('input'); + try0Checkbox.type = 'checkbox'; + try0Checkbox.id = 'try0'; + try0Checkbox.checked = true; + try0Label.appendChild(try0Checkbox); + try0Label.appendChild(document.createTextNode(' Try methods')); + + + proversInput.addEventListener('input', saveState); + isarCheckbox.addEventListener('change', saveState); + try0Checkbox.addEventListener('change', saveState); + const spinner = document.createElement('div'); + spinner.id = 'sledgehammer-spinner'; + + const applyBtn = document.createElement('button'); + applyBtn.textContent = 'Apply'; + applyBtn.id = 'apply-btn'; + applyBtn.addEventListener('click', () => { + wasCancelled = false; + kannbeCancelled = true; + result.innerHTML = ''; + addToHistory(proversInput.value); + hideDropdown(); + vscode.postMessage({ + command: 'apply', + provers: proversInput.value, + isar: isarCheckbox.checked, + try0: try0Checkbox.checked + }); + }); + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => { + vscode.postMessage({ command: 'cancel' }); + if (wasCancelled) return; + if (!kannbeCancelled) return; + wasCancelled = true; + spinner.classList.remove('loading'); + const div = document.createElement("div"); + div.classList.add("interrupt"); + div.textContent = "Interrupt"; + result.appendChild(div); + }); + + const locateBtn = document.createElement('button'); + locateBtn.textContent = 'Locate'; + locateBtn.addEventListener('click', () => { + vscode.postMessage({ + command: 'locate', + provers: proversInput.value, + isar: isarCheckbox.checked, + try0: try0Checkbox.checked + }); + }); + + topRow.appendChild(proversLabel); + topRow.appendChild(isarLabel); + topRow.appendChild(try0Label); + topRow.appendChild(spinner); + topRow.appendChild(applyBtn); + topRow.appendChild(cancelBtn); + topRow.appendChild(locateBtn); + container.appendChild(topRow); + + const result = document.createElement('div'); + result.id = 'result'; + container.appendChild(result); + + document.body.appendChild(container); + + renderDropdown(); + + window.addEventListener('message', event => { + const message = event.data; + if (message.command === 'status') { + spinner.classList.toggle('loading', message.message !== "Beendet"); + } + else if (message.command === 'provers') { + setHistory(message.history); + if (message.provers) { + proversInput.value = message.provers; + } else if (message.history && message.history.length > 0) { + proversInput.value = message.history[0]; + } + } + + else if (message.command === 'no_proof_context') { + const div = document.createElement("div"); + div.classList.add("interrupt"); + div.textContent = "Unknown proof context"; + result.appendChild(div); + } + else if (message.command === 'no_provers') { + const div = document.createElement("div"); + div.classList.add("interrupt"); + div.textContent = "No such provers"; + result.appendChild(div); + } + else if (message.command === 'result') { + if (wasCancelled) return; + result.innerHTML = ''; + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(`${message.content}`, 'application/xml'); + const messages = xmlDoc.getElementsByTagName('writeln_message'); + for (const msg of messages) { + const div = document.createElement('div'); + const inner = msg.innerHTML; + if (inner.includes(' { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text) { + const span = document.createElement('span'); + span.textContent = text; + div.appendChild(span); + } + } else if (node.nodeName.toLowerCase() === 'sendback') { + const button = document.createElement('button'); + button.textContent = node.textContent.trim(); + button.addEventListener('click', () => { + vscode.postMessage({ + command: 'insert_text', + provers: proversInput.value, + isar: isarCheckbox.checked, + try0: try0Checkbox.checked, + text: node.textContent.trim() + }); + }); + div.appendChild(button); + } else { + const span = document.createElement('span'); + span.textContent = node.textContent.trim(); + div.appendChild(span); + } + }); + } else { + div.textContent = msg.textContent.trim(); + } + result.appendChild(div); + } + if (/Unknown proof context/i.test(message.content)) { + result.classList.add('error'); + } else { + result.classList.remove('error'); + } + kannbeCancelled = false; + } + }); + + window.onload = function () { + const saved = vscode.getState(); + if (saved) { + history = Array.isArray(saved.history) ? saved.history : []; + proversInput.value = saved.provers || ''; + isarCheckbox.checked = !!saved.isar; + try0Checkbox.checked = saved.try0 !== undefined ? saved.try0 : true; + renderDropdown(false); + } + }; +})(); diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/symbol-panel.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/symbol-panel.svg Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,1 @@ + \ No newline at end of file diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/symbols.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/symbols.css Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,229 @@ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; + --input-border-color: var(--vscode-input-border, var(--vscode-panel-border)); +} + +--font-size-base: var(--vscode-editor-font-size); +--font-family-base: var(--vscode-editor-font-family); +--font-weight-base: var(--vscode-editor-font-weight); +--font-size-small: calc(var(--font-size-base) * 0.85); +--font-size-tiny: calc(var(--font-size-base) * 0.75); + + +body { + padding: 0 var(--container-paddding); + color: var(--vscode-foreground); + font-size: var(--font-size-base); + font-weight: var(--font-weight-base); + font-family: var(--font-family-base); + background-color: var(--vscode-sideBar-background); +} + +pre, code { + font-family: var(--font-family-base); + font-size: var(--font-size-base); +} + +a { + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +a:hover, a:active { + color: var(--vscode-textLink-activeForeground); +} + +ol, ul { + padding-left: var(--container-paddding); +} + +body > *, form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + + +.arrow-button, .abbrevs-button, .symbol-button, .reset-button, .control-button { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--vscode-panel-border); + border-radius: 2px; + cursor: pointer; + padding: 2px; + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + font-size: var(--font-size-small); + font-family: var(--vscode-editor-font-family); + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: auto; + height: auto; + min-width: 14px; + min-height: 14px; +} + +.arrow-button:hover, +.abbrevs-button:hover, +.symbol-button:hover, +.reset-button:hover, +.control-button:hover { + border: 1px solid var(--vscode-focusBorder); + background: var(--vscode-button-hoverBackground); +} + +.arrow-button:active, .abbrevs-button:active, .symbol-button:active, .reset-button:active, .control-button:active { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.tooltip { + position: absolute; + z-index: 9999; + background-color: var(--vscode-editorHoverWidget-background); + color: var(--vscode-editorHoverWidget-foreground); + padding: 4px 6px; + border-radius: 4px; + font-size: var(--font-size-small); + font-family: var(--font-family-base); + white-space: pre-line; + pointer-events: none; + opacity: 0; + transition: opacity 0.1s ease; + max-width: 250px; + box-shadow: 0 2px 4px var(--vscode-widget-shadow); +} +.tooltip.visible { opacity: 1; } + +.tabs { + display: flex; + flex-wrap: wrap; + background: var(--vscode-editor-background); + border-bottom: 1px solid var(--vscode-panel-border); +} + +.tab { + padding: 2px 4px; + cursor: pointer; + color: var(--vscode-foreground); + background: var(--vscode-editor-background); + border: none; + font-size: var(--font-size-small); + font-family: var(--font-family-base); +} +.tab.active { + border-bottom: 1.5px solid var(--vscode-focusBorder); +} +.tab:hover { + background: var(--vscode-list-hoverBackground); +} + +.content { padding: 5px; } + +.tab-content.hidden { display: none; } + + +.tab-content { + display: flex; + flex-wrap: wrap; + gap: 6px; + row-gap: 8px; +} + +.tab-content .symbol-button { margin: 0; } + +.tab-content .arrow-button, +.tab-content .abbrevs-button, +.tab-content .reset-button, +.tab-content .control-button { margin: 0; } + +input:not([type='checkbox']), textarea { + display: block; + width: 100%; + border: 1px solid var(--input-border-color); + font-family: var(--font-family-base); + font-size: var(--font-size-base); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); +} + +input::placeholder, textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +#controls { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} +#controls > * { + margin-left: 5px; + margin-top: 5px; +} + +pre { + white-space: pre-wrap; + word-wrap: break-word; +} + +.search-container { + width: 100%; + display: flex; + flex-direction: column; +} + +.search-input { + width: 100%; + font-family: var(--font-family-base); + font-size: var(--font-size-base); + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); + border: 1px solid var(--input-border-colorr); + padding: 4px 6px; + box-sizing: border-box; +} + +.search-input:hover { + border-color: var(--input-border-color); +} + +.search-input::placeholder { + animation: blink 1s steps(2, start) infinite; +} + +.search-input:focus { + border-color: var(--vscode-focusBorder); + outline: none; +} + +.search-results { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 6px; +} + +.search-results .symbol-button { + width: auto; + height: auto; + min-width: 14px; + min-height: 14px; + padding: 2px 6px; + box-sizing: border-box; +} + + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/media/symbols.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/media/symbols.js Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,348 @@ +(function () { + const vscode = acquireVsCodeApi(); + let allSymbols = {}; + let allAbbrevsFromThy = []; + let controlSup = ""; + let controlSub = ""; + + function decodeHtmlEntity(code) { + try { + return String.fromCodePoint(code); + } catch (error) { + return "?"; + } + } + + function formatGroupName(group) { + const groupNameMap = { + "Z_Notation": "Z Notation", + "Control_Block": "Control Block", + "Arrow": "Arrow", + "Control": "Control", + "Digit": "Digit", + "Document": "Document", + "Greek": "Greek", + "Icon": "Icon", + "Letter": "Letter", + "Logic": "Logic", + "Operator": "Operator", + "Punctuation": "Punctuation", + "Relation": "Relation", + "Unsorted": "Unsorted", + "Search": "Search" + }; + + return groupNameMap[group] || group.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + } + + function createSearchField() { + const searchContainer = document.createElement('div'); + searchContainer.classList.add('search-container'); + + const searchInput = document.createElement('input'); + searchInput.type = "text"; + searchInput.classList.add('search-input'); + + const searchResults = document.createElement('div'); + searchResults.classList.add('search-results'); + + searchInput.addEventListener('input', () => filterSymbols(searchInput.value, searchResults)); + + searchContainer.appendChild(searchInput); + searchContainer.appendChild(searchResults); + return { searchContainer, searchResults }; + } + + function filterSymbols(query, resultsContainer) { + const normalizedQuery = query.toLowerCase().trim(); + resultsContainer.innerHTML = ''; + + if (normalizedQuery === "") return; + + const matchingSymbols = []; + Object.values(allSymbols).forEach(symbolList => { + symbolList.forEach(symbol => { + if (!symbol.code) return; + + if ( + symbol.symbol.toLowerCase().includes(normalizedQuery) || + (symbol.abbrevs && symbol.abbrevs.some(abbr => abbr.toLowerCase().includes(normalizedQuery))) + ) { + matchingSymbols.push(symbol); + } + }); + }); + + const searchLimit = 50; + if (matchingSymbols.length === 0) { + resultsContainer.innerHTML = "

No symbols found

"; + } else { + matchingSymbols.slice(0, searchLimit).forEach(symbol => { + const decodedSymbol = decodeHtmlEntity(symbol.code); + if (!decodedSymbol) return; + + const button = document.createElement('div'); + button.classList.add('symbol-button'); + button.textContent = decodedSymbol; + button.setAttribute("data-tooltip", `${symbol.symbol}\nAbbreviations: ${symbol.abbrevs.join(', ')}`); + + button.addEventListener('click', () => { + vscode.postMessage({ command: 'insertSymbol', symbol: decodedSymbol }); + }); + + resultsContainer.appendChild(button); + }); + + if (matchingSymbols.length > searchLimit) { + const moreResults = document.createElement('div'); + moreResults.classList.add('more-results'); + moreResults.textContent = `(${matchingSymbols.length - searchLimit} more...)`; + resultsContainer.appendChild(moreResults); + } + } + } + + function renderWithEffects(symbol) { + if (!symbol) return ""; + + let result = ""; + let i = 0; + while (i < symbol.length) { + const char = symbol[i]; + if (char === "⇧") { + i++; + if (i < symbol.length) result += "" + symbol[i] + ""; + } else if (char === "⇩") { + i++; + if (i < symbol.length) result += "" + symbol[i] + ""; + } else { + result += char; + } + i++; + } + return result; + } + + function convertSymbolWithEffects(symbol) { + let result = ""; + let i = 0; + while (i < symbol.length) { + const char = symbol[i]; + if (char === "⇧") { + i++; + if (i < symbol.length) { + if (controlSup) result += controlSup + symbol[i]; + else result += symbol[i]; + } + } else if (char === "⇩") { + i++; + if (i < symbol.length) { + if (controlSub) result += controlSub + symbol[i]; + else result += symbol[i]; + } + } else { + result += char; + } + i++; + } + return result; +} + + function sanitizeSymbolForInsert(symbol) { + return symbol.replace(/\u0007/g, ''); + } + + function extractControlSymbols(symbolGroups) { + if (!symbolGroups || !symbolGroups["control"]) return; + symbolGroups["control"].forEach(symbol => { + if (symbol.name === "sup") controlSup = String.fromCodePoint(symbol.code); + if (symbol.name === "sub") controlSub = String.fromCodePoint(symbol.code); + }); + } + + function renderSymbols(groupedSymbols, abbrevsFromThy) { + const container = document.getElementById('symbols-container'); + container.innerHTML = ''; + + allSymbols = groupedSymbols; + allAbbrevsFromThy = abbrevsFromThy || []; + + extractControlSymbols(groupedSymbols); + + vscode.setState({ symbols: groupedSymbols, abbrevs_from_Thy: allAbbrevsFromThy }); + + if (!groupedSymbols || Object.keys(groupedSymbols).length === 0) { + container.innerHTML = "

No symbols available.

"; + return; + } + + const tabs = document.createElement('div'); + tabs.classList.add('tabs'); + + const content = document.createElement('div'); + content.classList.add('content'); + + Object.keys(groupedSymbols).forEach((group, index) => { + const tab = document.createElement('button'); + tab.classList.add('tab'); + tab.textContent = formatGroupName(group); + if (index === 0) tab.classList.add('active'); + + tab.addEventListener('click', () => { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); + document.getElementById(`content-${group}`).classList.remove('hidden'); + }); + + tabs.appendChild(tab); + + const groupContent = document.createElement('div'); + groupContent.classList.add('tab-content'); + groupContent.id = `content-${group}`; + if (index !== 0) groupContent.classList.add('hidden'); + + if (group === "control") { + const resetButton = document.createElement('button'); + resetButton.classList.add('reset-button'); + resetButton.textContent = "Reset"; + + + resetButton.addEventListener('click', () => { + vscode.postMessage({ command: 'resetControlSymbols' }); + }); + groupContent.appendChild(resetButton); + + const controlButtons = ["sup", "sub", "bold"]; + controlButtons.forEach(action => { + const controlSymbol = groupedSymbols[group].find(s => s.name === action); + if (controlSymbol) { + const decodedSymbol = decodeHtmlEntity(controlSymbol.code); + const button = document.createElement('button'); + button.classList.add('control-button'); + button.textContent = decodedSymbol; + button.title = action.charAt(0).toUpperCase() + action.slice(1); + button.addEventListener('click', () => { + vscode.postMessage({ command: 'applyControl', action: action }); + }); + groupContent.appendChild(button); + } + }); + } + + groupedSymbols[group].forEach(symbol => { + if (!symbol.code) return; + if (["sup", "sub", "bold"].includes(symbol.name)) return; + const decodedSymbol = decodeHtmlEntity(symbol.code); + if (!decodedSymbol) return; + + const button = document.createElement('div'); + if (group === "arrow") { + button.classList.add('arrow-button'); // Spezielle Klasse für Pfeile + } else { + button.classList.add('symbol-button'); + } + button.textContent = decodedSymbol; + button.setAttribute("data-tooltip", `${symbol.symbol}\nAbbreviations: ${symbol.abbrevs.join(', ')}`); + + button.addEventListener('click', () => { + vscode.postMessage({ command: 'insertSymbol', symbol: decodedSymbol }); + }); + + groupContent.appendChild(button); + }); + + content.appendChild(groupContent); + }); + + const abbrevsTab = document.createElement('button'); + abbrevsTab.classList.add('tab'); + abbrevsTab.textContent = "Abbrevs"; + abbrevsTab.addEventListener('click', () => { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + abbrevsTab.classList.add('active'); + document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); + document.getElementById("abbrevs-tab-content").classList.remove('hidden'); + }); + tabs.appendChild(abbrevsTab); + + const abbrevsContent = document.createElement('div'); + abbrevsContent.classList.add('tab-content', 'hidden'); + abbrevsContent.id = "abbrevs-tab-content"; + + allAbbrevsFromThy + .filter(([abbr, symbol]) => symbol && symbol.trim() !== "" && !/^[a-zA-Z0-9 _-]+$/.test(symbol)) + .forEach(([abbr, symbol]) => { + const btn = document.createElement('div'); + btn.classList.add('abbrevs-button'); + btn.innerHTML = renderWithEffects(symbol); + btn.title = abbr; + btn.addEventListener('click', () => { + vscode.postMessage({ command: 'insertSymbol', symbol: convertSymbolWithEffects(sanitizeSymbolForInsert(symbol)) }); + }); + + abbrevsContent.appendChild(btn); + }); + + content.appendChild(abbrevsContent); + + const searchTab = document.createElement('button'); + searchTab.classList.add('tab'); + searchTab.textContent = "Search"; + searchTab.addEventListener('click', () => { + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + searchTab.classList.add('active'); + document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); + document.getElementById("search-tab-content").classList.remove('hidden'); + }); + tabs.appendChild(searchTab); + + const { searchContainer, searchResults } = createSearchField(); + const searchContent = document.createElement('div'); + searchContent.classList.add('tab-content', 'hidden'); + searchContent.id = "search-tab-content"; + searchContent.appendChild(searchContainer); + + content.appendChild(searchContent); + container.appendChild(tabs); + container.appendChild(content); + + const tooltip = document.createElement("div"); + tooltip.className = "tooltip"; + document.body.appendChild(tooltip); + + document.querySelectorAll(".symbol-button, .arrow-button, .abbrevs-button").forEach(button => { + button.addEventListener("mouseenter", (e) => { + const rect = button.getBoundingClientRect(); + const text = button.getAttribute("data-tooltip"); + + if (text) { + tooltip.textContent = text; + tooltip.style.left = `${rect.left + window.scrollX}px`; + tooltip.style.top = `${rect.bottom + 6 + window.scrollY}px`; + tooltip.classList.add("visible"); + } + }); + + button.addEventListener("mouseleave", () => { + tooltip.classList.remove("visible"); + tooltip.textContent = ""; + }); + }); + + } + + window.addEventListener('message', event => { + if (event.data.command === 'update' && event.data.symbols) { + renderSymbols(event.data.symbols, event.data.abbrevs_from_Thy); + } + }); + + window.onload = function () { + const savedState = vscode.getState(); + if (savedState && savedState.symbols) { + renderSymbols(savedState.symbols, savedState.abbrevs_from_Thy); + } + }; + +})(); diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/package.json --- a/src/Tools/VSCode/extension/package.json Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/extension/package.json Thu Oct 23 16:15:40 2025 +0200 @@ -33,10 +33,36 @@ "id": "isabelle", "title": "Isabelle", "icon": "isabelle.png" + }, + { + "id": "isabelle-symbols", + "title": "Symbols", + "icon": "./media/symbol-panel.svg" + }, + { + "id": "isabelle-sledgehammer", + "title": "Sledgehammer", + "icon": "./media/sledgehammer-panel.svg" + } + ], + "activitybar": [ + { + "id": "isabelle-documentation", + "title": "Documentation", + "icon":"./media/documentation-panel.svg" } ] }, "views": { + "isabelle-symbols": [ + { "id": "isabelle-symbols", "name": "Symbols", "type": "webview", "icon": "./media/symbol-panel.svg" } + ], + "isabelle-documentation": [ + { "id": "isabelle-documentation", "name": "Documentation", "type": "webview", "icon": "./media/documentation-panel.svg" } + ], + "isabelle-sledgehammer": [ + { "id": "isabelle-sledgehammer", "name": "Sledgehammer", "type": "webview" , "icon": "./media/sledgehammer-panel.svg" } + ], "isabelle": [ { "type": "webview", @@ -126,6 +152,37 @@ "when": "editorLangId == bibtex", "command": "isabelle.preview-split", "group": "navigation" + }, + { + "when": "editorLangId == isabelle", + "command": "isabelle.openSymbolsPanel", + "group": "navigation" + }, + { + "when": "editorLangId == isabelle-ml", + "command": "isabelle.openSymbolsPanel", + "group": "navigation" + }, + { + "when": "editorLangId == isabelle", + "command": "isabelle.openDocumentationPanel", + "group": "navigation" + }, + { + "when": "editorLangId == isabelle-ml", + "command": "isabelle.openDocumentationPanel", + "group": "navigation" + }, + { + "when": "editorLangId == isabelle", + "command": "isabelle.openSledgehammerPanel", + "group": "navigation" + }, + { + "when": "editorLangId == isabelle-ml", + "command": "isabelle.openSledgehammerPanel", + "group": "navigation" + } ], "explorer/context": [ @@ -171,12 +228,12 @@ } ], "configurationDefaults": { - "[isabelle]": { - "files.encoding": "utf8isabelle" - }, - "[isabelle-ml]": { - "files.encoding": "utf8isabelle" - } + "[isabelle]": { + "files.encoding": "utf8isabelle" + }, + "[isabelle-ml]": { + "files.encoding": "utf8isabelle" + } }, "grammars": [ { diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/src/documentation_panel.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/src/documentation_panel.ts Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,158 @@ +/* Author: Diana Korchmar, LMU Muenchen + +Isabelle documentation panel as web view. +*/ + +'use strict'; + +import { + WebviewViewProvider, WebviewView, Uri, WebviewViewResolveContext, + CancellationToken, window, workspace, Webview, env +} from 'vscode' +import { text_colors } from './decorations' +import * as vscode_lib from './vscode_lib' +import * as path from 'path' +import * as lsp from './lsp' +import { commands } from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node'; + +class Documentation_Panel_Provider implements WebviewViewProvider { + public static readonly view_type = 'isabelle-documentation'; + + private _view?: WebviewView; + private _documentationSections: any[] = []; + private _initialized = false; + + constructor( + private readonly _extension_uri: Uri, + private readonly _language_client: LanguageClient + ) { } + + request(language_client: LanguageClient) { + if (language_client) { + this._language_client.sendNotification(lsp.documentation_request_type, { init: true }); + + } + } + + setupDocumentation(language_client: LanguageClient) { + language_client.onNotification(lsp.documentation_response_type, params => { + if (!params || !params.sections || !Array.isArray(params.sections)) { + return; + } + this._documentationSections = params.sections; + if (this._view) { + this._update_webview(); + } + }); + } + + public resolveWebviewView(view: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken): void { + this._view = view; + this._view.webview.options = { + enableScripts: true, + localResourceRoots: [ + this._extension_uri + ] + }; + + this._view.webview.html = this._get_html(); + + if (Object.keys(this._documentationSections).length > 0) { + this._update_webview(); + } + + this._view.webview.onDidReceiveMessage(async message => { + if (message.command === 'openFile') { + this._open_document(message.path); + } + }); + + this._initialized = true; + } + + private _update_webview(): void { + if (!this._view) { + return; + } + + this._view.webview.postMessage({ + command: 'update', + sections: this._documentationSections, + }); + } + + private _open_document(filePath: string): void { + let cleanPath = filePath.replace(/^"+|"+$/g, '').trim(); + + const isabelleHome = process.env.ISABELLE_HOME; + if (isabelleHome && cleanPath.includes("$ISABELLE_HOME")) { + cleanPath = cleanPath.replace("$ISABELLE_HOME", isabelleHome); + } + + if (cleanPath.startsWith("/cygdrive/")) { + const match = cleanPath.match(/^\/cygdrive\/([a-zA-Z])\/(.*)/); + if (match) { + const driveLetter = match[1].toUpperCase(); + const rest = match[2].replace(/\//g, '\\'); + cleanPath = `${driveLetter}:\\${rest}`; + } + } + + const uri = Uri.file(cleanPath); + + if (cleanPath.toLowerCase().endsWith(".pdf")) { + commands.executeCommand("vscode.open", uri) + } else { + workspace.openTextDocument(uri).then(document => { + window.showTextDocument(document); + }); + } + } + + + private _get_html(): string { + return get_webview_html(this._view?.webview, this._extension_uri.fsPath); + } +} + +function get_webview_html(webview: Webview | undefined, extension_path: string): string { + const script_uri = webview.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'documentation.js'))) + const css_uri = webview.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'documentation.css'))) + const font_uri = + webview.asWebviewUri(Uri.file(path.join(extension_path, 'fonts', 'IsabelleDejaVuSansMono.ttf'))) + + return ` + + + + + + + + Documentation Panel + + +
Loading documentation...
+ + + `; +} + +function _get_decorations(): string { + let style: string[] = [] + for (const key of text_colors) { + style.push(`body.vscode-light .${key} { color: ${vscode_lib.get_color(key, true)} }\n`) + style.push(`body.vscode-dark .${key} { color: ${vscode_lib.get_color(key, false)} }\n`) + } + return style.join("") +} + +export { Documentation_Panel_Provider, get_webview_html }; + diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/src/extension.ts --- a/src/Tools/VSCode/extension/src/extension.ts Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/extension/src/extension.ts Thu Oct 23 16:15:40 2025 +0200 @@ -16,9 +16,12 @@ import * as lsp from './lsp' import * as state_panel from './state_panel' import { Uri, TextEditor, ViewColumn, Selection, Position, ExtensionContext, workspace, window, - commands, ProgressLocation } from 'vscode' + commands, ProgressLocation, Range } from 'vscode' import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node' import { Output_View_Provider } from './output_view' +import { Symbols_Panel_Provider } from './symbol_panel' +import { Documentation_Panel_Provider } from './documentation_panel' +import { Sledgehammer_Panel_Provider } from './sledgehammer_panel' import { register_script_decorations } from './script_decorations' @@ -206,6 +209,46 @@ params => provider.update_content(params.content)) }) + const documentation_provider = new Documentation_Panel_Provider(context.extensionUri, language_client); + context.subscriptions.push(window.registerWebviewViewProvider(Documentation_Panel_Provider.view_type, documentation_provider)); + + language_client.onReady().then(() => { + documentation_provider.request(language_client); + documentation_provider.setupDocumentation(language_client); + }); + + const symbols_provider = new Symbols_Panel_Provider(context.extensionUri, language_client); + context.subscriptions.push( + window.registerWebviewViewProvider(Symbols_Panel_Provider.view_type, symbols_provider) + ); + language_client.onReady().then(() => symbols_provider.request(language_client)); + language_client.onReady().then(() => symbols_provider.setup(language_client)); + + + const sledgehammer_provider = new Sledgehammer_Panel_Provider(context.extensionUri, language_client); + context.subscriptions.push( + window.registerWebviewViewProvider(Sledgehammer_Panel_Provider.view_type, sledgehammer_provider) + ); + language_client.onReady().then(() => sledgehammer_provider.request_provers(language_client)) + + language_client.onReady().then(() => { + language_client.onNotification(lsp.sledgehammer_status_type, msg => + sledgehammer_provider.updateStatus(msg.message)) + language_client.onNotification(lsp.sledgehammer_apply_response_type, msg => + sledgehammer_provider.updateResult(msg)) + language_client.onNotification(lsp.sledgehammer_no_such_prover_type, msg => + sledgehammer_provider.updateNoProver(msg)) + language_client.onNotification(lsp.sledgehammer_insert_position_response_type, msg => + sledgehammer_provider.insert(msg.position)) + language_client.onNotification(lsp.sledgehammer_locate_response_type, msg => + sledgehammer_provider.locate(msg.position)) + language_client.onNotification(lsp.sledgehammer_provers_response, msg => { + sledgehammer_provider.update_provers(msg.provers, msg.history) + }) + language_client.onNotification(lsp.sledgehammer_no_proof_context_type, () => + sledgehammer_provider.updateNoProofContext()); + }) + /* state panel */ diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/src/lsp.ts --- a/src/Tools/VSCode/extension/src/lsp.ts Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/extension/src/lsp.ts Thu Oct 23 16:15:40 2025 +0200 @@ -129,6 +129,136 @@ content: string } +export interface Symbol_Entry { + name: string; + abbrevs: string[]; + groups: string[]; + code?: number; + font?: string; + symbol: string; + argument: string; + decoded: string; +} + +export interface Symbols_Response { + symbols: Symbol_Entry []; + abbrevs_from_Thy: [string, string][]; +} + +export const symbols_response_type = + new NotificationType('PIDE/symbols_response'); + +export interface InitRequest { + init: boolean; +} + +export const symbols_request_type = + new NotificationType("PIDE/symbols_panel_request") + + +export const documentation_request_type = + new NotificationType("PIDE/documentation_request") + +export interface Documentation_Response { + sections: Array<{ + title: string; + important: boolean; + entries: Array<{ title: string; path: string }>; + }>; +} + +export const documentation_response_type = + new NotificationType("PIDE/documentation_response"); + +export interface SledgehammerApplyRequest { + provers: string; + isar: boolean; + try0: boolean; + purpose: number; +} + +export interface SledgehammerStatus { + message: string; +} + +export interface SledgehammerApplyResult { + content: string; + position: { + uri: string; + line: number; + character: number; + }; + sendbackId: number; + state_location: { + uri: string; + line: number; + character: number; + }; +} + +export interface SledgehammerLocate { + position: { + uri: string; + line: number; + character: number; + }; +} + +export interface SledgehammerInsertPosition { + position: { + uri: string; + line: number; + character: number; + }; +} + +export interface Sledgehammer_Provers { + provers: string; + history: string[]; +} + +export interface Sledgehammer_Provers_Delete { + entry: string; +} + +export interface SledgehammerNoSuchProver { + provers: string[]; +} + +export const sledgehammer_request_type = + new NotificationType('PIDE/sledgehammer_request'); + +export const sledgehammer_provers_delete = + new NotificationType('PIDE/sledgehammer_deleteProvers_request'); + +export const sledgehammer_cancel_type = + new NotificationType('PIDE/sledgehammer_cancel_request'); + +export const sledgehammer_provers = + new NotificationType('PIDE/sledgehammer_provers_request'); + +export const sledgehammer_provers_response = + new NotificationType('PIDE/sledgehammer_provers_response'); + +export const sledgehammer_no_such_prover_type = + new NotificationType('PIDE/sledgehammer_noProver_response'); + +export const sledgehammer_status_type = + new NotificationType('PIDE/sledgehammer_status_response'); + +export const sledgehammer_apply_response_type = + new NotificationType('PIDE/sledgehammer_apply_response'); + +export const sledgehammer_locate_response_type = + new NotificationType('PIDE/sledgehammer_locate_response'); + +export const sledgehammer_insert_position_response_type = + new NotificationType('PIDE/sledgehammer_insert_position_response'); + +export const sledgehammer_no_proof_context_type = + new NotificationType('PIDE/sledgehammer_no_proof_context'); + + export const preview_request_type = new NotificationType("PIDE/preview_request") diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/src/sledgehammer_panel.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/src/sledgehammer_panel.ts Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,218 @@ +/* Author: Diana Korchmar, LMU Muenchen + +Isabelle sledgehammer panel as web view. +*/ + +import { WebviewViewProvider, WebviewView, Uri, WebviewViewResolveContext, CancellationToken, window, Webview, Selection, Range, TextDocument, TextDocumentContentChangeEvent } from 'vscode'; +import * as path from 'path'; +import { text_colors } from './decorations'; +import * as vscode_lib from './vscode_lib' +import * as lsp from './lsp'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { Position } from 'vscode'; + + +class Sledgehammer_Panel_Provider implements WebviewViewProvider { + public static readonly view_type = 'isabelle-sledgehammer'; + private _view?: WebviewView; + private _lastResultPosition?: { uri: string; line: number; character: number; lineText: string }; + private text_to_insert: string; + + constructor( + private readonly _extension_uri: Uri, + private readonly _language_client: LanguageClient + ) { } + + public resolveWebviewView(view: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken): void { + this._view = view; + this._view.webview.options = { + enableScripts: true, + localResourceRoots: [this._extension_uri] + }; + this._view.webview.html = this._get_html(); + this._setup_message_handler(); + } + + request_provers(language_client: LanguageClient) { + if (language_client) { + this._language_client.sendNotification(lsp.sledgehammer_provers, { init: true }); + } + } + + private _setup_message_handler(): void { + if (!this._view) return; + this._view.webview.onDidReceiveMessage(async message => { + const editor = window.activeTextEditor; + const pos = editor?.selection.active; + + if (editor && pos) { + this._language_client.sendNotification(lsp.caret_update_type, { + uri: editor.document.uri.toString(), + line: pos.line, + character: pos.character + }); + } + switch (message.command) { + case 'apply': + this._language_client.sendNotification(lsp.sledgehammer_request_type, { + provers: message.provers, + isar: message.isar, + try0: message.try0, + purpose: 1 + }); + + break; + case 'cancel': + this._language_client.sendNotification(lsp.sledgehammer_cancel_type); + break; + case 'locate': + this._language_client.sendNotification(lsp.sledgehammer_request_type, { + provers: message.provers, + isar: message.isar, + try0: message.try0, + purpose: 2 + }); + break; + + case 'insert_text': + this._language_client.sendNotification(lsp.sledgehammer_request_type, { + provers: message.provers, + isar: message.isar, + try0: message.try0, + purpose: 3 + }); + this.text_to_insert = message.text; + break; + + case 'delete_prover_history': + this._language_client.sendNotification(lsp.sledgehammer_provers_delete, { + entry: message.entry + }); + break; + + } + }); + } + + public updateStatus(message: string): void { + if (this._view) { + this._view.webview.postMessage({ command: 'status', message }); + } + } + + public update_provers(provers: string, history: string[]): void { + if (this._view) { + this._view.webview.postMessage({ command: 'provers', provers, history }); + } + } + + public locate(state_location: { uri: string; line: number; character: number }): void { + const docUri = Uri.parse(state_location.uri); + const editor = window.activeTextEditor; + + if (editor && editor.document.uri.toString() === docUri.toString()) { + const position = new Position(state_location.line, state_location.character); + editor.selections = [new Selection(position, position)]; + editor.revealRange(new Range(position, position)); + } + } + + public insert(position: { uri: string; line: number; character: number }): void { + const editor = window.activeTextEditor; + if (!editor) return; + + const uri = Uri.parse(position.uri); + if (editor.document.uri.toString() !== uri.toString()) return; + + const pos = new Position(position.line, position.character); + const existingLineText = editor.document.lineAt(pos.line).text; + const textToInsert = existingLineText.trim() === '' ? this.text_to_insert : '\n' + this.text_to_insert; + + editor.edit(editBuilder => { + editBuilder.insert(pos, textToInsert); + }); + } + + + public updateResult(result: lsp.SledgehammerApplyResult): void { + const editor = window.activeTextEditor; + const lineText = editor?.document.lineAt(result.position.line).text ?? ""; + this._lastResultPosition = { + ...result.position, + lineText + }; + + if (this._view) { + this._view.webview.postMessage({ + command: 'result', + content: result.content, + position: result.position, + sendbackId: result.sendbackId + }); + + } + } + + public updateNoProver(provers: lsp.SledgehammerNoSuchProver): void { + if (this._view) { + this._view.webview.postMessage({ command: 'no_provers' }); + } + } + + + public updateNoProofContext(): void { + if (this._view) { + this._view.webview.postMessage({ command: 'no_proof_context' }); + } + } + + private _get_html(): string { + return get_webview_html(this._view?.webview, this._extension_uri.fsPath); + } +} + +function get_webview_html(webview: Webview | undefined, extension_path: string): string { + const script_uri = webview?.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'sledgehammer.js'))); + const css_uri = webview?.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'sledgehammer.css'))); + const font_uri = + webview.asWebviewUri(Uri.file(path.join(extension_path, 'fonts', 'IsabelleDejaVuSansMono.ttf'))) + + + return ` + + + + + + + + Sledgehammer Panel + + + + + `; +} + +function _get_decorations(): string { + let style: string[] = [] + for (const key of text_colors) { + style.push(`body.vscode-light .${key} { color: ${vscode_lib.get_color(key, true)} }\n`) + style.push(`body.vscode-dark .${key} { color: ${vscode_lib.get_color(key, false)} }\n`) + } + return style.join("") +} + +export { Sledgehammer_Panel_Provider, get_webview_html }; + +export interface PositionInfo { + uri: string; + line: number; + character: number; +} diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/extension/src/symbol_panel.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/extension/src/symbol_panel.ts Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,275 @@ +/* Author: Diana Korchmar, LMU Muenchen + +Isabelle symbols panel as web view. +*/ + +'use strict'; + +import { + WebviewViewProvider, WebviewView, Uri, WebviewViewResolveContext, + CancellationToken, window, Position, Selection, Webview +} from 'vscode' +import { text_colors } from './decorations' +import * as vscode_lib from './vscode_lib' +import * as path from 'path' +import * as lsp from './lsp' +import { LanguageClient } from 'vscode-languageclient/node'; +import * as fs from 'fs'; + +class Symbols_Panel_Provider implements WebviewViewProvider { + public static readonly view_type = 'isabelle-symbols' + + private _view?: WebviewView + private _groupedSymbols: { [key: string]: lsp.Symbol_Entry[] } = {} + private _initialized = false; + private _abbrevsFromThy: [string, string][] = []; + + constructor( + private readonly _extension_uri: Uri, + private readonly _language_client: LanguageClient + ) { } + + request( language_client: LanguageClient) { + if (language_client) { + this._language_client.sendNotification(lsp.symbols_request_type, { init: true }); + } + } + + setup(language_client: LanguageClient) { + language_client.onNotification(lsp.symbols_response_type, params => { + if (!params?.symbols || !Array.isArray(params.symbols)) { + return; + + } + + this._groupedSymbols = this._group_symbols(params.symbols); + this._abbrevsFromThy = params.abbrevs_from_Thy ?? []; + if (this._view) { + this._update_webview(); + } + }); + } + + public resolveWebviewView( + view: WebviewView, + context: WebviewViewResolveContext, + _token: CancellationToken) { + this._view = view + + view.webview.options = { + enableScripts: true, + + localResourceRoots: [ + this._extension_uri + ] + } + + view.webview.html = this._get_html() + + if (Object.keys(this._groupedSymbols).length > 0) { + this._update_webview(); + } + + this._view.webview.onDidReceiveMessage(message => { + if (message.command === 'insertSymbol') { + this._insert_symbol(message.symbol); + } else if (message.command === 'resetControlSymbols') { + this._reset_control_symbols(); + } else if (message.command === 'applyControl') { + this._apply_control_effect(message.action); + } + }); + + + this._initialized = true; + } + + private _apply_control_effect(action: string): void { + const editor = window.activeTextEditor; + if (!editor) return; + + const document = editor.document; + const selection = editor.selection; + let selectedText = document.getText(selection); + + if (!selectedText.trim() && !selection.isEmpty) return; + + const controlGroup = this._groupedSymbols["control"]; + if (!controlGroup) return; + + const controlSymbols: { [key: string]: string } = {}; + controlGroup.forEach(symbol => { + if (symbol.name === "sup" || symbol.name === "sub" || symbol.name === "bold") { + controlSymbols[symbol.name] = String.fromCodePoint(symbol.code); + } + }); + + if (!controlSymbols[action]) return; + const controlSymbol = controlSymbols[action]; + const allControlSymbols = Object.values(controlSymbols); + + + editor.edit(editBuilder => { + if (!selection.isEmpty) { + if (action === "bold") { + editor.setDecorations(this.boldDecoration, [{ range: selection }]); + } else { + let newText = selectedText + .split("") + .map((char, index, arr) => { + const prevChar = index > 0 ? arr[index - 1] : null; + if (char.trim() === "") return char; + if (allControlSymbols.includes(char)) return ""; + + return `${controlSymbol}${char}`; + }) + .join(""); + + editBuilder.replace(selection, newText); + } + } else { + editBuilder.insert(selection.active, controlSymbol); + } + }).then(success => { + if (!success) { + window.showErrorMessage("Failed to apply control effect."); + } + }); + } + + private _insert_symbol(symbol: string): void { + const editor = window.activeTextEditor; + if (editor) { + editor.edit(editBuilder => { + editBuilder.insert(editor.selection.active, symbol); + }); + } + } + + private boldDecoration = window.createTextEditorDecorationType({ + fontWeight: "bold", + textDecoration: "none" + }); + + private _reset_control_symbols(): void { + const editor = window.activeTextEditor; + if (!editor) { + return; + } + + const document = editor.document; + const selection = editor.selection; + let selectedText = document.getText(selection); + + if (!selectedText.trim() && !selection.isEmpty) return; + + const controlGroup = this._groupedSymbols["control"]; + if (!controlGroup) return; + + + const controlSymbols: { [key: string]: string } = {}; + controlGroup.forEach(symbol => { + if (symbol.name === "sup" || symbol.name === "sub" || symbol.name === "bold") { + controlSymbols[String.fromCodePoint(symbol.code)] = symbol.name; + } + }); + + const allControlSymbols = Object.keys(controlSymbols); + + editor.edit(editBuilder => { + if (!selection.isEmpty) { + if (this.boldDecoration) { + editor.setDecorations(this.boldDecoration, []); + } + + let newText = selectedText + .split("") + .map(char => (allControlSymbols.includes(char) ? "" : char)) + .join(""); + + editBuilder.replace(selection, newText); + } + }).then(success => { + if (!success) { + } + }); + } + + private _update_webview(): void { + this._view.webview.postMessage({ + command: 'update', + symbols: this._groupedSymbols, + abbrevs_from_Thy: this._abbrevsFromThy, + }); + } + + private _group_symbols(symbols: lsp.Symbol_Entry[]): { [key: string]: lsp.Symbol_Entry[] } { + const groupedSymbols: { [key: string]: lsp.Symbol_Entry[] } = {}; + symbols.forEach(symbol => { + if (!symbol.groups || !Array.isArray(symbol.groups)) { + return; + } + + symbol.groups.forEach(group => { + if (!groupedSymbols[group]) { + groupedSymbols[group] = []; + } + groupedSymbols[group].push(symbol); + }); + }); + return groupedSymbols; + } + + private _get_html(): string { + return get_webview_html(this._view.webview, this._extension_uri.fsPath) + } +} + +function open_webview_link(link: string) { + const uri = Uri.parse(link) + const line = Number(uri.fragment) || 0 + const pos = new Position(line, 0) + window.showTextDocument(uri.with({ fragment: '' }), { + preserveFocus: false, + selection: new Selection(pos, pos) + }) +} + +function get_webview_html(webview: Webview, extension_path: string): string { + const script_uri = webview.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'symbols.js'))) + const css_uri = webview.asWebviewUri(Uri.file(path.join(extension_path, 'media', 'symbols.css'))) + const font_uri = + webview.asWebviewUri(Uri.file(path.join(extension_path, 'fonts', 'IsabelleDejaVuSansMono.ttf'))) + + return ` + + + + + + + Symbols Panel + + +
+ + + ` +} + +function _get_decorations(): string { + let style: string[] = [] + for (const key of text_colors) { + style.push(`body.vscode-light .${key} { color: ${vscode_lib.get_color(key, true)} }\n`) + style.push(`body.vscode-dark .${key} { color: ${vscode_lib.get_color(key, false)} }\n`) + } + return style.join("") +} + +export { Symbols_Panel_Provider, get_webview_html, open_webview_link } diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/src/language_server.scala --- a/src/Tools/VSCode/src/language_server.scala Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/src/language_server.scala Thu Oct 23 16:15:40 2025 +0200 @@ -14,7 +14,7 @@ import isabelle._ import java.io.{PrintStream, OutputStream, File => JFile} - +import isabelle.vscode.Sledgehammer_Panel import scala.annotation.tailrec @@ -117,6 +117,7 @@ private val session_ = Synchronized(None: Option[VSCode_Session]) def session: VSCode_Session = session_.value getOrElse error("Server inactive") def resources: VSCode_Resources = session.resources + private val sledgehammer_panel = Sledgehammer_Panel(this) def rendering_offset(node_pos: Line.Node_Position): Option[(VSCode_Rendering, Text.Offset)] = for { @@ -296,6 +297,7 @@ session.syslog_messages += syslog_messages dynamic_output.init() + sledgehammer_panel.init() try { Isabelle_Process.start( @@ -324,6 +326,7 @@ delay_output.revoke() delay_caret_update.revoke() delay_preview.revoke() + sledgehammer_panel.exit() val result = session.stop() if (result.ok) reply("") @@ -487,6 +490,20 @@ channel.write(LSP.Symbols_Convert_Request.reply(id, converted)) } + def symbols_panel_request(init: Boolean): Unit = { + val abbrevs = + resources.get_caret().flatMap { caret => + resources.get_model(caret.file).map(_.syntax().abbrevs) + }.getOrElse(session.resources.session_base.overall_syntax.abbrevs) + + channel.write(LSP.Symbols_Response(Symbol.symbols, abbrevs)) + } + + + def documentation_request(init: Boolean): Unit = { + channel.write(LSP.Documentation_Response()) + } + /* main loop */ @@ -530,6 +547,12 @@ case LSP.Symbols_Convert_Request(id, text, boolean) => symbols_convert_request(id, text, boolean) case LSP.Preview_Request(file, column) => preview_request(file, column) + case LSP.Symbols_Panel_Request(init) => symbols_panel_request(init) + case LSP.Documentation_Request(init) => documentation_request(init) + case LSP.Sledgehammer_Request(provers, isar, try0, purpose) => sledgehammer_panel.handle_request(provers, isar, try0, purpose) + case LSP.Sledgehammer_Cancel => sledgehammer_panel.cancel_query() + case LSP.Sledgehammer_Provers(init) => sledgehammer_panel.send_provers_and_history(init) + case LSP.Sledgehammer_Delete_Prover(entry) => sledgehammer_panel.handle_sledgehammer_delete(entry) case _ => if (!LSP.ResponseMessage.is_empty(json)) log("### IGNORED") } } diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/src/lsp.scala --- a/src/Tools/VSCode/src/lsp.scala Thu Oct 23 14:49:21 2025 +0200 +++ b/src/Tools/VSCode/src/lsp.scala Thu Oct 23 16:15:40 2025 +0200 @@ -737,4 +737,152 @@ "label" -> label, "content" -> content)) } + + object Symbols_Panel_Request { + def unapply(json: JSON.T): Option[(Boolean)] = + json match { + case Notification("PIDE/symbols_panel_request", Some(params)) => + for { + init <- JSON.bool(params, "init") + } yield (init) + case _ => None + } + } + + object Documentation_Request { + def unapply(json: JSON.T): Option[(Boolean)] = + json match { + case Notification("PIDE/documentation_request", Some(params)) => + for { + init <- JSON.bool(params, "init") + } yield (init) + case _ => None + } + } + + + object Sledgehammer_Provers { + def unapply(json: JSON.T): Option[(Boolean)] = + json match { + case Notification("PIDE/documentation_request", Some(params)) => + for { + init <- JSON.bool(params, "init") + } yield (init) + case _ => None + } + } + + + object Symbols_Response { + def apply(symbols: Symbol.Symbols, abbrevs: List[(String, String)]): JSON.T = { + def json(symbol: Symbol.Entry): JSON.T = { + val decoded = Symbol.decode(symbol.symbol) + + JSON.Object( + "symbol" -> symbol.symbol, + "name" -> symbol.name, + "argument" -> symbol.argument.toString, + "decoded" -> decoded + ) ++ + JSON.optional("code", symbol.code) ++ + JSON.optional("font", symbol.font) ++ + JSON.Object( + "groups" -> symbol.groups, + "abbrevs" -> symbol.abbrevs + ) + } + + + Notification("PIDE/symbols_response", JSON.Object("symbols" -> symbols.entries.map(json), "abbrevs_from_Thy" -> abbrevs.map { case (a, b) => List(a, b) } + )) + } + } + + + object Documentation_Response { + def apply(): JSON.T = { + val ml_settings = ML_Settings.init() + val doc_contents = Doc.contents(ml_settings) + val json_sections = doc_contents.sections.map { section => + JSON.Object( + "title" -> section.title, + "important" -> section.important, + "entries" -> section.entries.map { entry => + JSON.Object("title" -> entry.title, "path" -> entry.path.toString) + } + ) + } + + Notification("PIDE/documentation_response", JSON.Object("sections" -> json_sections)) + } + } + + object Sledgehammer_Request { + def unapply(json: JSON.T): Option[(String, Boolean, Boolean, Int)] = + json match { + case Notification("PIDE/sledgehammer_request", Some(params)) => + for { + provers <- JSON.string(params, "provers") + isar <- JSON.bool(params, "isar") + try0 <- JSON.bool(params, "try0") + purpose <- JSON.int(params, "purpose") orElse Some(1) // fallback default + } yield (provers, isar, try0, purpose) + case _ => None + } + } + + object Sledgehammer_Delete_Prover { + def unapply(json: JSON.T): Option[String] = + json match { + case Notification("PIDE/sledgehammer_deleteProvers_request", Some(params)) => + JSON.string(params, "entry") + case _ => None + } + } + + object Sledgehammer_Cancel extends Notification0("PIDE/sledgehammer_cancel_request") + + object Sledgehammer_Provers_Response { + def apply(provers: String, history: List[String]): JSON.T = { + Notification( + "PIDE/sledgehammer_provers_response", + JSON.Object("provers" -> provers, "history" -> history) + ) + } + } + + object Sledgehammer_NoProver_Response { + def apply(provers: List[String]): JSON.T = + Notification("PIDE/sledgehammer_noProver_response", JSON.Object("provers" -> provers)) + } + + object Sledgehammer_Status_Response { + def apply(message: String): JSON.T = + Notification("PIDE/sledgehammer_status_response", JSON.Object("message" -> message)) + } + + object Sledgehammer_Apply_Response { + def apply(resultJson: JSON.Object.T): JSON.T = { + Notification("PIDE/sledgehammer_apply_response", resultJson) + } + } + + object Sledgehammer_Locate_Response { + def apply(resultJson: JSON.Object.T): JSON.T = { + Notification("PIDE/sledgehammer_locate_response", resultJson) + } + } + + object Sledgehammer_InsertPosition_Response { + def apply(resultJson: JSON.Object.T): JSON.T = { + Notification("PIDE/sledgehammer_insert_position_response", resultJson) + } + } + + object Sledgehammer_NoProof_Response{ + def apply(): JSON.T = + Notification("PIDE/sledgehammer_no_proof_context", None) + } + } + diff -r 3de18e94ac7c -r 486e094b676c src/Tools/VSCode/src/sledgehammer_panel.scala --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/Tools/VSCode/src/sledgehammer_panel.scala Thu Oct 23 16:15:40 2025 +0200 @@ -0,0 +1,242 @@ +/* Title: Tools/VSCode/src/sledgehammer_panel.scala + Author: Diana Korchmar +*/ + +package isabelle.vscode + + +import isabelle._ + +import java.io.{PrintStream, PrintWriter, FileWriter, OutputStream, File => JFile} + +object Sledgehammer_Panel { + def apply(server: Language_Server): Sledgehammer_Panel = + new Sledgehammer_Panel(server) +} + +class Sledgehammer_Panel private(server: Language_Server) { + private val query_operation = + new Query_Operation(server.editor, (), "sledgehammer", consume_status, consume_result) + var current_purpose: Int = 1 + private var last_sendback_id: Option[Int] = None + + private val provers_history_option: String = "sledgehammer_provers_history" + private val provers_history_delim: Char = '|' + + private var persistent_options: Options = Options.init() + + private def get_provers_history(): List[String] = + Library.space_explode(provers_history_delim, persistent_options.string(provers_history_option)).filterNot(_.isEmpty) + + private def set_provers_history(new_history: String): Unit = { + persistent_options = persistent_options.string.update(provers_history_option, new_history) + persistent_options.save_prefs() + } + + def send_provers_and_history(init: Boolean): Unit = { + val provers = persistent_options.check_name("sledgehammer_provers").value + val history = get_provers_history() + server.channel.write(LSP.Sledgehammer_Provers_Response.apply(provers, history)) + } + + def add_provers_history(entry: String): Unit = { + val current = get_provers_history() + val history = (entry :: current.filter(_ != entry)).take(10) + val history_str = history.mkString(provers_history_delim.toString) + set_provers_history(history_str) + } + + def handle_request(provers: String, isar: Boolean, try0: Boolean, purpose: Int): Unit = { + val available_provers = persistent_options.string("sledgehammer_provers").split("\\s+").toSet + val user_provers = provers.trim.split("\\s+").toSet + val invalid = user_provers.diff(available_provers) + if (invalid.nonEmpty) { + server.channel.write(LSP.Sledgehammer_NoProver_Response.apply(invalid.toList)) + return + } + choose_purpose(List(provers, isar.toString, try0.toString), purpose) + add_provers_history(provers) + } + + def handle_sledgehammer_delete(entry: String): Unit = { + val history = get_provers_history().filter(_ != entry) + val history_str = history.mkString(provers_history_delim.toString) + set_provers_history(history_str) + } + + private def reload_persistent_options(): Unit = { + persistent_options = Options.init() + } + + private def consume_status(status: Query_Operation.Status): Unit = { + val msg = status match { + case Query_Operation.Status.waiting => "Warte auf Kontext..." + case Query_Operation.Status.running => "Sledgehammer läuft..." + case Query_Operation.Status.finished => "Beendet" + } + if (msg.nonEmpty) server.channel.write(LSP.Sledgehammer_Status_Response(msg)) + } + + private def extractSendbackId(body: List[XML.Elem]): Option[Int] = { + def traverse(tree: XML.Tree): Option[Int] = tree match { + case XML.Elem(markup, body) if markup.name == "sendback" => + markup.properties.find(_._1 == "id").flatMap { + case (_, id_str) => scala.util.Try(id_str.toInt).toOption + }.orElse(body.view.flatMap(traverse).headOption) + + case XML.Elem(_, body) => + body.view.flatMap(traverse).headOption + + case _ => None + } + body.view.flatMap(traverse).headOption + } + + private def count_lines(text: String, offset: Int): Int = + text.substring(0, offset).count(_ == '\n') + + private def count_column(text: String, offset: Int): Int = { + val lastNewline = text.substring(0, offset).lastIndexOf('\n') + if (lastNewline >= 0) offset - lastNewline - 1 else offset + } + + private def resolvePosition(snapshot: Document.Snapshot, sendbackId: Int): Option[(String, Int, Int)] = { + snapshot.node.commands.find(_.id == sendbackId).flatMap { command => + snapshot.node.command_iterator().find(_._1 == command).map { + case (_, start_offset) => + val end_offset = start_offset + command.length // Hier nehmen wir das Ende des Command! + val text = snapshot.node.source + val line = count_lines(text, end_offset) + val column = count_column(text, end_offset) + val uri = Url.print_file(new java.io.File(snapshot.node_name.node)) + (uri, line, column) + } + } + } + + private def query_position_from_sendback(snapshot: Document.Snapshot, sendbackId: Int): Option[(String, Int, Int)] = { + val node = snapshot.node + val iterator = node.command_iterator().toList + + iterator.find(_._1.id == sendbackId).map { case (command, start_offset) => + val text = node.source + val line = count_lines(text, start_offset) + val column = count_column(text, start_offset) + val uri = Url.print_file(new java.io.File(snapshot.node_name.node)) + + val snippet = text.substring(start_offset, (start_offset + command.length).min(text.length)).replace("\n", "\\n") + + (uri, line, column) + } + } + + private def consume_result(snapshot: Document.Snapshot, results: Command.Results, body: List[XML.Elem]): Unit = { + val xmlString = body.map(XML.string_of_tree).mkString + + if (xmlString.contains("Done")) { + val sendbackIdOpt = extractSendbackId(body) + last_sendback_id = sendbackIdOpt + + val position = sendbackIdOpt.flatMap(id => resolvePosition(snapshot, id)) + .getOrElse(("unknown", 0, 0)) + + val query_position = sendbackIdOpt.flatMap(id => query_position_from_sendback(snapshot, id)) + .getOrElse(("unknown", 0, 0)) + + val text = snapshot.node.source + + val resultJson = JSON.Object( + "content" -> xmlString, + "position" -> JSON.Object( + "uri" -> position._1, + "line" -> position._2, + "character" -> position._3 + ), + "sendbackId" -> sendbackIdOpt.getOrElse(-1), + "state_location" -> JSON.Object( + "uri" -> query_position._1, + "line" -> query_position._2, + "character" -> query_position._3) + ) + + server.channel.write(LSP.Sledgehammer_Apply_Response(resultJson)) + } + } + + def choose_purpose(args: List[String], purpose: Int): Unit = { + current_purpose = purpose + purpose match { + case 1 => + server.resources.get_caret() match { + case Some(caret) => + val snapshot = server.resources.snapshot(caret.model) + val uri = Url.print_file(caret.file) + query_operation.apply_query(args) + case None => + server.channel.write(LSP.Sledgehammer_NoProof_Response()) + } + + case 2 => + locate() + + case 3 => + insert_query() + + case _ => + } + } + + def locate(): Unit = { + for { + sendbackId <- last_sendback_id + caret <- server.resources.get_caret() + snapshot = server.resources.snapshot(caret.model) + query_position <- query_position_from_sendback(snapshot, sendbackId) + } { + val json = JSON.Object( + "position" -> JSON.Object( + "uri" -> query_position._1, + "line" -> query_position._2, + "character" -> query_position._3 + ) + ) + server.channel.write(LSP.Sledgehammer_Locate_Response(json)) + } + } + + def insert_query(): Unit = { + last_sendback_id match { + case Some(sendbackId) => + val models = server.resources.get_models() + val modelOpt = models.find { model => + val snapshot = server.resources.snapshot(model) + val contains = snapshot.node.commands.exists(_.id == sendbackId) + contains + } + + modelOpt match { + case Some(model) => + val snapshot = server.resources.snapshot(model) + resolvePosition(snapshot, sendbackId) match { + case Some((uri, line, col)) => + val json = JSON.Object( + "position" -> JSON.Object( + "uri" -> uri, + "line" -> line, + "character" -> col + ) + ) + server.channel.write(LSP.Sledgehammer_InsertPosition_Response(json)) + case None => + } + case None => + } + case None => + } + } + + def cancel_query(): Unit = query_operation.cancel_query() + def locate_query(): Unit = query_operation.locate_query() + def init(): Unit = query_operation.activate() + def exit(): Unit = query_operation.deactivate() +}