Compare commits
10 Commits
9281d1d724
...
feature/wa
| Author | SHA1 | Date | |
|---|---|---|---|
| 4037e9e2d7 | |||
| 4b2f6967dc | |||
| 79ef879b97 | |||
| 1df7462778 | |||
| 0bcdc615e3 | |||
| 5577fe17bb | |||
| 820ee2f848 | |||
| be8365c8cb | |||
| b1dd2442b8 | |||
| 226bb46094 |
1027
docs/数据流程详解.md
Normal file
1027
docs/数据流程详解.md
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/index.html
Normal file
200
media/surfer/index.html
Normal file
@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<!-- Disable zooming: -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<head>
|
||||
<!-- change this to your project name -->
|
||||
<title>Surfer</title>
|
||||
|
||||
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||
<script type="module">
|
||||
import init from '/surfer.js';
|
||||
await init({module_or_path: '/surfer_bg.wasm'});
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
/*SURFER_SETUP_HOOKS*/
|
||||
</script>
|
||||
|
||||
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||
<base href="/" />
|
||||
|
||||
|
||||
<script>
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Setting error message")
|
||||
document.getElementById("error_message").innerHTML = msg
|
||||
document.getElementById("error_container").style.display = "block"
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||
|
||||
<style>
|
||||
html {
|
||||
/* Remove touch delay: */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Light mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
/* Dark mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow canvas to fill entire web page: */
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Make canvas fill entire document: */
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #f0f0f0;
|
||||
font-size: 24px;
|
||||
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- */
|
||||
/* Loading animation from https://loading.io/css/ */
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_container a {
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h1>Sorry, Surfer crashed 🔥</h1>
|
||||
<p>
|
||||
Something caused Surfer to crash. Please report the error on
|
||||
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
|
||||
gitlab
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
|
||||
the crash and/or the steps to reproduce the crash.
|
||||
</p>
|
||||
<h3>
|
||||
Backtrace:
|
||||
</h3>
|
||||
<div class="error_container">
|
||||
<!-- This is filled in by javascript -->
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register the message listener system -->
|
||||
<script src="integration.js"></script>
|
||||
<script>
|
||||
register_message_listener()
|
||||
</script>
|
||||
|
||||
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||
<script>
|
||||
// We disable caching during development so that we always view the latest version.
|
||||
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
||||
65
media/surfer/integration.js
Normal file
65
media/surfer/integration.js
Normal file
@ -0,0 +1,65 @@
|
||||
// Web apps which integrate Surfer as an iframe can give commands to surfer via
|
||||
// the .postMessage [1] function on the iframe.
|
||||
//
|
||||
// For example, to tell Surfer to load waveforms from a URL, use
|
||||
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
|
||||
//
|
||||
// For more complex functionality, one can also inject any `Message` defined
|
||||
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
|
||||
// is not stable and may change at any time. If you add functionality via
|
||||
// these, make sure to test the new functionality when changing Surfer version.
|
||||
//
|
||||
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
|
||||
|
||||
function register_message_listener() {
|
||||
window.addEventListener("message", (event) => {
|
||||
// JSON decode the message
|
||||
const decoded = event.data
|
||||
|
||||
switch (decoded.command) {
|
||||
// Load a waveform from a URL. The format is inferred from the data.
|
||||
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
|
||||
|
||||
case 'LoadUrl': {
|
||||
const msg = {
|
||||
LoadWaveformFileFromUrl: [
|
||||
decoded.url,
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ToggleMenu': {
|
||||
const msg = "ToggleMenu"
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Load waveform data directly from string content
|
||||
case 'LoadData': {
|
||||
const msg = {
|
||||
LoadFromData: [
|
||||
decoded.content,
|
||||
decoded.fileName || "waveform.vcd",
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Inject any other message supported by Surfer in the surfer::Message enum.
|
||||
// NOTE: The API of these is unstable.
|
||||
case 'InjectMessage': {
|
||||
inject_message(decoded.message);
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unknown message.command ${decoded.command}`)
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
10
media/surfer/manifest.json
Normal file
10
media/surfer/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"display": "standalone",
|
||||
"id": "/index.html",
|
||||
"lang": "en-US",
|
||||
"name": "Surfer",
|
||||
"short_name": "surfer",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "white"
|
||||
}
|
||||
2227
media/surfer/surfer.js
Normal file
2227
media/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
200
media/surfer/surfer/index.html
Normal file
200
media/surfer/surfer/index.html
Normal file
@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<!-- Disable zooming: -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<head>
|
||||
<!-- change this to your project name -->
|
||||
<title>Surfer</title>
|
||||
|
||||
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||
<script type="module">
|
||||
import init from '/surfer.js';
|
||||
await init({module_or_path: '/surfer_bg.wasm'});
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
/*SURFER_SETUP_HOOKS*/
|
||||
</script>
|
||||
|
||||
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||
<base href="/" />
|
||||
|
||||
|
||||
<script>
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Setting error message")
|
||||
document.getElementById("error_message").innerHTML = msg
|
||||
document.getElementById("error_container").style.display = "block"
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||
|
||||
<style>
|
||||
html {
|
||||
/* Remove touch delay: */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Light mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
/* Dark mode background color for what is not covered by the egui canvas,
|
||||
or where the egui canvas is translucent. */
|
||||
background: #404040;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allow canvas to fill entire web page: */
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Make canvas fill entire document: */
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #f0f0f0;
|
||||
font-size: 24px;
|
||||
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- */
|
||||
/* Loading animation from https://loading.io/css/ */
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: #ffffff;
|
||||
background-color: black;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_container a {
|
||||
color: #ff9999;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h1>Sorry, Surfer crashed 🔥</h1>
|
||||
<p>
|
||||
Something caused Surfer to crash. Please report the error on
|
||||
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
|
||||
gitlab
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
|
||||
the crash and/or the steps to reproduce the crash.
|
||||
</p>
|
||||
<h3>
|
||||
Backtrace:
|
||||
</h3>
|
||||
<div class="error_container">
|
||||
<!-- This is filled in by javascript -->
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register the message listener system -->
|
||||
<script src="integration.js"></script>
|
||||
<script>
|
||||
register_message_listener()
|
||||
</script>
|
||||
|
||||
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||
<script>
|
||||
// We disable caching during development so that we always view the latest version.
|
||||
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<!-- Powered by egui: https://github.com/emilk/egui/ -->
|
||||
52
media/surfer/surfer/integration.js
Normal file
52
media/surfer/surfer/integration.js
Normal file
@ -0,0 +1,52 @@
|
||||
// Web apps which integrate Surfer as an iframe can give commands to surfer via
|
||||
// the .postMessage [1] function on the iframe.
|
||||
//
|
||||
// For example, to tell Surfer to load waveforms from a URL, use
|
||||
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
|
||||
//
|
||||
// For more complex functionality, one can also inject any `Message` defined
|
||||
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
|
||||
// is not stable and may change at any time. If you add functionality via
|
||||
// these, make sure to test the new functionality when changing Surfer version.
|
||||
//
|
||||
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
|
||||
|
||||
function register_message_listener() {
|
||||
window.addEventListener("message", (event) => {
|
||||
// JSON decode the message
|
||||
const decoded = event.data
|
||||
|
||||
switch (decoded.command) {
|
||||
// Load a waveform from a URL. The format is inferred from the data.
|
||||
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
|
||||
|
||||
case 'LoadUrl': {
|
||||
const msg = {
|
||||
LoadWaveformFileFromUrl: [
|
||||
decoded.url,
|
||||
"Clear"
|
||||
]
|
||||
}
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ToggleMenu': {
|
||||
const msg = "ToggleMenu"
|
||||
inject_message(JSON.stringify(msg))
|
||||
break;
|
||||
}
|
||||
|
||||
// Inject any other message supported by Surfer in the surfer::Message enum.
|
||||
// NOTE: The API of these is unstable.
|
||||
case 'InjectMessage': {
|
||||
inject_message(decoded.message);
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`Unknown message.command ${decoded.command}`)
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
10
media/surfer/surfer/manifest.json
Normal file
10
media/surfer/surfer/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"display": "standalone",
|
||||
"id": "/index.html",
|
||||
"lang": "en-US",
|
||||
"name": "Surfer",
|
||||
"short_name": "surfer",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "white"
|
||||
}
|
||||
2227
media/surfer/surfer/surfer.js
Normal file
2227
media/surfer/surfer/surfer.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
BIN
media/surfer/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/surfer/sw.js
Normal file
37
media/surfer/surfer/sw.js
Normal file
@ -0,0 +1,37 @@
|
||||
self.addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(function (response) {
|
||||
// It seems like we only need to set the headers for index.html
|
||||
// If you want to be on the safe side, comment this out
|
||||
// if (!response.url.includes("index.html")) return response;
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
const moddedResponse = new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
|
||||
return moddedResponse;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error(e);
|
||||
})
|
||||
);
|
||||
});
|
||||
BIN
media/surfer/surfer_bg.wasm
Normal file
BIN
media/surfer/surfer_bg.wasm
Normal file
Binary file not shown.
37
media/surfer/sw.js
Normal file
37
media/surfer/sw.js
Normal file
@ -0,0 +1,37 @@
|
||||
self.addEventListener("install", function () {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function (event) {
|
||||
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(function (response) {
|
||||
// It seems like we only need to set the headers for index.html
|
||||
// If you want to be on the safe side, comment this out
|
||||
// if (!response.url.includes("index.html")) return response;
|
||||
|
||||
const newHeaders = new Headers(response.headers);
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
|
||||
const moddedResponse = new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
|
||||
return moddedResponse;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error(e);
|
||||
})
|
||||
);
|
||||
});
|
||||
12
package.json
12
package.json
@ -70,6 +70,18 @@
|
||||
"id": "iccoder",
|
||||
"label": "IC Coder"
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "ic-coder.vcdViewer",
|
||||
"displayName": "VCD 波形查看器",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.vcd"
|
||||
}
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
BIN
rustup-init.exe
Normal file
BIN
rustup-init.exe
Normal file
Binary file not shown.
237
src/components/codeHighlight.ts
Normal file
237
src/components/codeHighlight.ts
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 代码高亮组件
|
||||
*
|
||||
* 功能说明:
|
||||
* - 使用 highlight.js 提供专业的代码语法高亮
|
||||
* - 支持多种编程语言(Verilog, JavaScript, Python 等)
|
||||
* - 提供行内代码和代码块的不同样式
|
||||
* - 自动检测语言类型
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取 highlight.js 的 CDN 链接
|
||||
*/
|
||||
export function getHighlightJsLinks(): string {
|
||||
return `
|
||||
<!-- Highlight.js CSS (VS Code Dark+ 主题) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
|
||||
<!-- Highlight.js 核心库 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<!-- Verilog 语言支持 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/verilog.min.js"></script>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码高亮的样式
|
||||
*/
|
||||
export function getCodeHighlightStyles(): string {
|
||||
return `
|
||||
/* 代码块基础样式 */
|
||||
.segment-text pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.segment-text pre code {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
border: none;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
font-family: 'Courier New', Consolas, 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 行内代码样式 */
|
||||
.segment-text code:not(pre code) {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
font-family: 'Courier New', Consolas, 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 覆盖 highlight.js 的背景色,使用 VSCode 主题色 */
|
||||
.segment-text pre code.hljs {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 代码块语言标签 */
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
margin: -20px 0;
|
||||
}
|
||||
|
||||
.code-language-label {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--vscode-badge-background);
|
||||
color: var(--vscode-badge-foreground);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 代码块复制按钮 */
|
||||
.code-copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.code-block-wrapper:hover .code-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.code-copy-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.code-copy-btn.copied {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
/* 代码块滚动条样式 */
|
||||
.segment-text pre::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-track {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.segment-text pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代码高亮的脚本
|
||||
*/
|
||||
export function getCodeHighlightScript(): string {
|
||||
return `
|
||||
/**
|
||||
* 使用 highlight.js 进行代码高亮
|
||||
*/
|
||||
function highlightCodeBlocks() {
|
||||
// 等待 highlight.js 加载完成
|
||||
if (typeof hljs === 'undefined') {
|
||||
setTimeout(highlightCodeBlocks, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const codeBlocks = document.querySelectorAll('.segment-text pre code:not(.hljs)');
|
||||
codeBlocks.forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为代码块添加复制按钮
|
||||
*/
|
||||
function enhanceCodeBlocks() {
|
||||
const codeBlocks = document.querySelectorAll('.segment-text pre code');
|
||||
|
||||
codeBlocks.forEach((codeElement) => {
|
||||
const preElement = codeElement.parentElement;
|
||||
if (!preElement || preElement.classList.contains('enhanced')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记为已增强,避免重复处理
|
||||
preElement.classList.add('enhanced');
|
||||
|
||||
// 应用语法高亮
|
||||
if (typeof hljs !== 'undefined' && !codeElement.classList.contains('hljs')) {
|
||||
hljs.highlightElement(codeElement);
|
||||
}
|
||||
|
||||
// 创建包装器
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block-wrapper';
|
||||
preElement.parentNode.insertBefore(wrapper, preElement);
|
||||
wrapper.appendChild(preElement);
|
||||
|
||||
// 添加复制按钮
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'code-copy-btn';
|
||||
copyBtn.textContent = '复制';
|
||||
copyBtn.onclick = function() {
|
||||
const code = codeElement.textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyBtn.textContent = '已复制';
|
||||
copyBtn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = '复制';
|
||||
copyBtn.classList.remove('copied');
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
wrapper.appendChild(copyBtn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 DOM 变化,自动增强新添加的代码块
|
||||
*/
|
||||
function observeCodeBlocks() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
enhanceCodeBlocks();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.getElementById('messages'), {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化代码块增强
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
enhanceCodeBlocks();
|
||||
observeCodeBlocks();
|
||||
});
|
||||
} else {
|
||||
enhanceCodeBlocks();
|
||||
observeCodeBlocks();
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -8,10 +8,10 @@ import * as vscode from "vscode";
|
||||
type Environment = "dev" | "test" | "prod";
|
||||
|
||||
/** 当前环境 - 修改这里切换环境 */
|
||||
const CURRENT_ENV: Environment = "dev";
|
||||
const CURRENT_ENV: Environment = "test";
|
||||
|
||||
/** 服务等级类型 */
|
||||
export type ServiceTier = 'lite' | 'syntaxic' | 'max' | 'auto';
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/** 配置项接口 */
|
||||
export interface IccoderConfig {
|
||||
@ -32,7 +32,7 @@ const ENV_CONFIG: Record<Environment, IccoderConfig> = {
|
||||
backendUrl: "http://localhost:2233",
|
||||
timeout: 300000,
|
||||
userId: "default-user",
|
||||
serviceTier: "max", // 默认使用 max
|
||||
serviceTier: "max", // 默认使用 max
|
||||
},
|
||||
/** 测试服务器环境 */
|
||||
test: {
|
||||
|
||||
@ -170,3 +170,8 @@ export const stateTransitionIconSvg = `
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
|
||||
/**
|
||||
* 用户提问图标 SVG
|
||||
*/
|
||||
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;
|
||||
|
||||
@ -1,13 +1,27 @@
|
||||
import * as vscode from "vscode";
|
||||
import { ICViewProvider } from "./views/ICViewProvider";
|
||||
import { showICHelperPanel } from "./panels/ICHelperPanel";
|
||||
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
|
||||
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "./utils/chatHistoryManager";
|
||||
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
|
||||
import { VCDFileServer } from "./services/vcdFileServer";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log("🎉 IC Coder 插件已激活!");
|
||||
|
||||
// 初始化 VCD 文件服务器
|
||||
const vcdFileServer = new VCDFileServer();
|
||||
vcdFileServer.start().then((port) => {
|
||||
console.log(`VCD 文件服务器已启动,端口: ${port}`);
|
||||
}).catch((error) => {
|
||||
console.error("启动 VCD 文件服务器失败:", error);
|
||||
});
|
||||
|
||||
// 在插件停用时关闭服务器
|
||||
context.subscriptions.push({
|
||||
dispose: () => vcdFileServer.stop()
|
||||
});
|
||||
|
||||
// 注册 Authentication Provider
|
||||
const authProvider = new ICCoderAuthenticationProvider(context);
|
||||
context.subscriptions.push(
|
||||
@ -68,7 +82,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath);
|
||||
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath, vcdFileServer);
|
||||
}
|
||||
);
|
||||
|
||||
@ -160,6 +174,9 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
viewProvider
|
||||
);
|
||||
|
||||
// 注册 VCD 自定义编辑器
|
||||
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
|
||||
|
||||
// 添加到订阅
|
||||
context.subscriptions.push(
|
||||
openPanelCommand,
|
||||
@ -174,7 +191,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// deleteSessionCommand,
|
||||
// clearHistoryCommand,
|
||||
// searchSessionCommand,
|
||||
viewRegistration
|
||||
viewRegistration,
|
||||
vcdEditorProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -176,12 +176,10 @@ export async function showICHelperPanel(
|
||||
vscode.window.showInformationMessage(message.text);
|
||||
break;
|
||||
case "openWaveformViewer":
|
||||
// 打开波形查看器
|
||||
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
|
||||
if (message.vcdFilePath) {
|
||||
VCDViewerPanel.createOrShow(
|
||||
context.extensionUri,
|
||||
message.vcdFilePath
|
||||
);
|
||||
const vcdUri = vscode.Uri.file(message.vcdFilePath);
|
||||
vscode.commands.executeCommand('vscode.open', vcdUri);
|
||||
}
|
||||
break;
|
||||
case "getVCDInfo":
|
||||
|
||||
@ -1,19 +1,77 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { VCDFileServer } from "../services/vcdFileServer";
|
||||
|
||||
/**
|
||||
* VCD 波形查看器面板
|
||||
* VCD 波形查看器自定义编辑器提供者
|
||||
*/
|
||||
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
|
||||
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
|
||||
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
|
||||
const providerRegistration = vscode.window.registerCustomEditorProvider(
|
||||
"ic-coder.vcdViewer",
|
||||
provider,
|
||||
{
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
return providerRegistration;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly vcdFileServer: VCDFileServer
|
||||
) {}
|
||||
|
||||
async openCustomDocument(
|
||||
uri: vscode.Uri,
|
||||
openContext: vscode.CustomDocumentOpenContext,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CustomDocument> {
|
||||
return {
|
||||
uri,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
async resolveCustomEditor(
|
||||
document: vscode.CustomDocument,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
webviewPanel.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this.context.extensionUri],
|
||||
};
|
||||
|
||||
// 使用公共工厂方法创建 VCD 查看器实例
|
||||
VCDViewerPanel.createFromWebviewPanel(
|
||||
webviewPanel,
|
||||
this.context.extensionUri,
|
||||
document.uri.fsPath,
|
||||
this.vcdFileServer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VCD 波形查看器面板 (使用 Surfer)
|
||||
*/
|
||||
export class VCDViewerPanel {
|
||||
public static currentPanel: VCDViewerPanel | undefined;
|
||||
private readonly _panel: vscode.WebviewPanel;
|
||||
private readonly _extensionUri: vscode.Uri;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _currentVcdPath: string | undefined;
|
||||
private _vcdFileServer: VCDFileServer | undefined;
|
||||
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
|
||||
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
|
||||
this._panel = panel;
|
||||
this._extensionUri = extensionUri;
|
||||
this._vcdFileServer = vcdFileServer;
|
||||
|
||||
// 设置初始 HTML 内容
|
||||
this._panel.webview.html = this._getLoadingHtml();
|
||||
@ -24,12 +82,20 @@ export class VCDViewerPanel {
|
||||
// 监听来自 webview 的消息
|
||||
this._panel.webview.onDidReceiveMessage(
|
||||
(message) => {
|
||||
console.log("[VCDViewerPanel] 收到消息:", message);
|
||||
switch (message.command) {
|
||||
case "loadVCD":
|
||||
if (message.filePath) {
|
||||
this.loadVCDFile(message.filePath);
|
||||
}
|
||||
break;
|
||||
case "loaded":
|
||||
// Surfer iframe 加载完成,发送 VCD 文件
|
||||
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
|
||||
if (this._currentVcdPath) {
|
||||
this.sendVcdToSurfer(this._currentVcdPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
null,
|
||||
@ -40,7 +106,7 @@ export class VCDViewerPanel {
|
||||
/**
|
||||
* 创建或显示 VCD 查看器面板
|
||||
*/
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
|
||||
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
|
||||
const column = vscode.ViewColumn.One;
|
||||
|
||||
// 如果已经有面板打开,则显示它
|
||||
@ -64,7 +130,7 @@ export class VCDViewerPanel {
|
||||
}
|
||||
);
|
||||
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri);
|
||||
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
|
||||
// 如果提供了 VCD 文件路径,加载它
|
||||
if (vcdFilePath) {
|
||||
@ -72,23 +138,44 @@ export class VCDViewerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已有的 webview panel 创建 VCD 查看器(用于自定义编辑器)
|
||||
*/
|
||||
public static createFromWebviewPanel(
|
||||
panel: vscode.WebviewPanel,
|
||||
extensionUri: vscode.Uri,
|
||||
vcdFilePath: string,
|
||||
vcdFileServer?: VCDFileServer
|
||||
) {
|
||||
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
|
||||
viewer.loadVCDFile(vcdFilePath);
|
||||
return viewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 VCD 文件
|
||||
*/
|
||||
public loadVCDFile(vcdFilePath: string) {
|
||||
try {
|
||||
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(vcdFilePath)) {
|
||||
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前 VCD 路径
|
||||
this._currentVcdPath = vcdFilePath;
|
||||
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
|
||||
|
||||
// 更新面板标题
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
this._panel.title = `VCD 波形查看器 - ${fileName}`;
|
||||
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
|
||||
|
||||
// 设置 HTML 内容
|
||||
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
|
||||
this._panel.webview.html = this._getWebviewContent();
|
||||
console.log("[VCDViewerPanel] Webview HTML 已设置");
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
@ -96,6 +183,104 @@ export class VCDViewerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 VCD 文件获取根模块及其直接子模块名称
|
||||
*/
|
||||
private parseVcdRootScope(vcdFilePath: string): string[] {
|
||||
try {
|
||||
// 读取 VCD 文件
|
||||
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
const scopeNames: string[] = [];
|
||||
let scopeDepth = 0;
|
||||
const scopeStack: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// 遇到 $enddefinitions 就停止解析
|
||||
if (trimmed.startsWith('$enddefinitions')) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 查找 $scope 定义
|
||||
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
|
||||
if (scopeMatch) {
|
||||
const scopeType = scopeMatch[1];
|
||||
const scopeName = scopeMatch[2];
|
||||
|
||||
// 记录顶层 module (depth = 0)
|
||||
if (scopeDepth === 0 && scopeType === 'module') {
|
||||
scopeStack.push(scopeName);
|
||||
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
|
||||
}
|
||||
// 记录顶层下的直接子模块 (depth = 1)
|
||||
else if (scopeDepth === 1 && scopeType === 'module') {
|
||||
const fullPath = [...scopeStack, scopeName];
|
||||
scopeNames.push(fullPath.join('.'));
|
||||
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
|
||||
}
|
||||
|
||||
scopeDepth++;
|
||||
}
|
||||
|
||||
// 遇到 $upscope 减少深度
|
||||
if (trimmed.startsWith('$upscope')) {
|
||||
scopeDepth--;
|
||||
if (scopeDepth === 0) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeNames;
|
||||
} catch (error) {
|
||||
console.error("[VCDViewerPanel] 解析 VCD 文件失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 VCD 文件到 Surfer
|
||||
*/
|
||||
private sendVcdToSurfer(vcdFilePath: string) {
|
||||
try {
|
||||
console.log("[VCDViewerPanel] 准备发送 VCD 到 Surfer:", vcdFilePath);
|
||||
|
||||
if (!this._vcdFileServer) {
|
||||
throw new Error("VCD 文件服务器未初始化");
|
||||
}
|
||||
|
||||
// 解析 VCD 文件获取根模块名称
|
||||
const scopeNames = this.parseVcdRootScope(vcdFilePath);
|
||||
console.log("[VCDViewerPanel] 解析到的作用域名称:", scopeNames);
|
||||
|
||||
// 注册文件到 HTTP 服务器
|
||||
const fileId = this._vcdFileServer.registerFile(vcdFilePath);
|
||||
const httpUrl = this._vcdFileServer.getFileUrl(fileId);
|
||||
const fileName = path.basename(vcdFilePath);
|
||||
|
||||
console.log("[VCDViewerPanel] 文件名:", fileName);
|
||||
console.log("[VCDViewerPanel] HTTP URL:", httpUrl);
|
||||
|
||||
// 使用 LoadUrl 命令通过 HTTP 加载文件
|
||||
this._panel.webview.postMessage({
|
||||
command: "loadVcdUrl",
|
||||
url: httpUrl,
|
||||
fileName: fileName,
|
||||
scopeNames: scopeNames, // 传递解析到的作用域名称
|
||||
});
|
||||
|
||||
console.log("[VCDViewerPanel] 已发送 loadVcdUrl 消息到 webview");
|
||||
} catch (error) {
|
||||
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
|
||||
vscode.window.showErrorMessage(
|
||||
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
@ -163,188 +348,239 @@ export class VCDViewerPanel {
|
||||
/**
|
||||
* 获取 Webview 的 HTML 内容
|
||||
*/
|
||||
private _getWebviewContent(vcdFilePath: string): string {
|
||||
// 获取资源 URI
|
||||
const vcdromJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
|
||||
private _getWebviewContent(): string {
|
||||
// 获取 surfer 资源 URI
|
||||
const surferJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
|
||||
);
|
||||
const vcdWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
|
||||
const surferWasmUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
|
||||
);
|
||||
const fontRegularUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
|
||||
const integrationJsUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
|
||||
);
|
||||
const fontObliqueUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Oblique.woff2")
|
||||
);
|
||||
const fontItalicUri = this._panel.webview.asWebviewUri(
|
||||
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Italic.woff2")
|
||||
);
|
||||
|
||||
// 读取 VCD 文件内容并转换为 base64
|
||||
const vcdContent = fs.readFileSync(vcdFilePath, "utf-8");
|
||||
const vcdBase64 = Buffer.from(vcdContent).toString("base64");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${this._panel.webview.cspSource}; style-src 'unsafe-inline' ${this._panel.webview.cspSource}; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; img-src ${this._panel.webview.cspSource} data:; connect-src ${this._panel.webview.cspSource};">
|
||||
<title>VCD 波形查看器</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
|
||||
<title>Surfer 波形查看器</title>
|
||||
|
||||
<script>
|
||||
// 获取 VS Code API(只能调用一次)
|
||||
const vscode = acquireVsCodeApi();
|
||||
window.vscode = vscode;
|
||||
window.surferReady = false;
|
||||
window.pendingVcdData = null;
|
||||
|
||||
function on_surfer_error(msg) {
|
||||
console.log("Surfer error:", msg);
|
||||
document.getElementById("error_message").innerHTML = msg;
|
||||
document.getElementById("error_container").style.display = "block";
|
||||
}
|
||||
window.on_surfer_error = on_surfer_error;
|
||||
|
||||
// 加载 VCD URL 的函数
|
||||
function loadVcdUrl(data) {
|
||||
try {
|
||||
console.log('[Webview] ========== 开始加载 VCD URL ==========');
|
||||
console.log('[Webview] URL:', data.url);
|
||||
console.log('[Webview] Scope names from VCD:', data.scopeNames);
|
||||
|
||||
// 使用 setTimeout 确保 Surfer 完全准备好
|
||||
setTimeout(() => {
|
||||
console.log('[Webview] 通过 postMessage 发送 LoadUrl 命令');
|
||||
|
||||
// 使用 integration.js 提供的标准 LoadUrl 命令
|
||||
window.postMessage({
|
||||
command: 'LoadUrl',
|
||||
url: data.url
|
||||
}, '*');
|
||||
|
||||
console.log('[Webview] ✅ 已发送 LoadUrl 命令');
|
||||
|
||||
// 等待文件加载完成后,自动添加所有信号
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('[Webview] Attempting to add all signals automatically');
|
||||
|
||||
// 使用从 VCD 文件解析出来的作用域名称
|
||||
let scopeNamesToTry = [];
|
||||
|
||||
if (data.scopeNames && data.scopeNames.length > 0) {
|
||||
// 使用解析出来的实际子模块路径(例如 "tb.dut")
|
||||
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
|
||||
console.log('[Webview] Using parsed scope names:', scopeNamesToTry);
|
||||
} else {
|
||||
// 回退到常见的根作用域名称
|
||||
scopeNamesToTry = [
|
||||
['top'],
|
||||
['testbench'],
|
||||
['tb'],
|
||||
['test'],
|
||||
['dut']
|
||||
];
|
||||
console.log('[Webview] Using fallback scope names');
|
||||
}
|
||||
|
||||
for (let i = 0; i < scopeNamesToTry.length; i++) {
|
||||
const scopeName = scopeNamesToTry[i];
|
||||
try {
|
||||
const addScopeMsg = {
|
||||
"AddScope": [
|
||||
{
|
||||
"strs": scopeName,
|
||||
"id": {"Wellen": i + 1}
|
||||
},
|
||||
true // 递归添加子模块的所有信号
|
||||
]
|
||||
};
|
||||
window.inject_message(JSON.stringify(addScopeMsg));
|
||||
console.log('[Webview] Sent AddScope for: ' + scopeName.join('.') + ' (recursive)');
|
||||
} catch (e) {
|
||||
console.log('[Webview] Failed for scope: ' + scopeName.join('.'), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待信号加载完成后,自动缩放到全部时间范围
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ZoomToFit"));
|
||||
console.log('[Webview] Sent ZoomToFit command');
|
||||
} catch (e) {
|
||||
console.log('[Webview] ZoomToFit failed:', e);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Webview] Failed to add signals:', e);
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Webview] ❌ 加载 VCD 失败:', error);
|
||||
on_surfer_error(error.message + '\\n' + error.stack);
|
||||
}
|
||||
}
|
||||
window.loadVcdUrl = loadVcdUrl;
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
console.log('[Webview] 开始初始化 Surfer...');
|
||||
import init from '${surferJsUri}';
|
||||
await init({module_or_path: '${surferWasmUri}'});
|
||||
console.log('[Webview] Surfer WASM 已加载');
|
||||
|
||||
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '${surferJsUri}';
|
||||
window.inject_message = inject_message;
|
||||
window.id_of_name = id_of_name;
|
||||
window.draw_text_arrow = draw_text_arrow;
|
||||
|
||||
console.log('[Webview] Surfer 函数已导入,inject_message 类型:', typeof window.inject_message);
|
||||
|
||||
// 等待一小段时间确保 Surfer 完全初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
window.surferReady = true;
|
||||
console.log('[Webview] Surfer 已完全初始化并准备就绪');
|
||||
|
||||
// 关闭 Surfer 的日志面板(如果打开的话)
|
||||
try {
|
||||
window.inject_message(JSON.stringify("ToggleLogs"));
|
||||
console.log('[Webview] 已发送关闭日志面板命令');
|
||||
} catch (e) {
|
||||
console.log('[Webview] 关闭日志面板失败:', e);
|
||||
}
|
||||
|
||||
// 如果有待处理的 VCD 数据,现在加载它
|
||||
if (window.pendingVcdData) {
|
||||
console.log('[Webview] 发现待处理的 VCD 数据,立即加载');
|
||||
loadVcdUrl(window.pendingVcdData);
|
||||
window.pendingVcdData = null;
|
||||
} else {
|
||||
console.log('[Webview] 没有待处理的 VCD 数据');
|
||||
}
|
||||
|
||||
// 通知 VS Code surfer 已加载完成
|
||||
console.log('[Webview] 发送 loaded 消息到 VS Code');
|
||||
window.vscode.postMessage({ command: 'loaded' });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
src: url('${fontRegularUri}') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: oblique;
|
||||
src: url('${fontObliqueUri}') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Iosevka Drom Web';
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
font-stretch: normal;
|
||||
font-style: italic;
|
||||
src: url('${fontItalicUri}') format('woff2');
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iosevka Drom Web', monospace;
|
||||
color: var(--vscode-foreground);
|
||||
background-color: var(--vscode-editor-background);
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
#waveform-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#waveform1 {
|
||||
canvas {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid var(--vscode-progressBar-background);
|
||||
border-top: 4px solid var(--vscode-progressBar-foreground);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
#error_container {
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 0px auto;
|
||||
max-width: 980px;
|
||||
color: var(--vscode-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
border-radius: 4px;
|
||||
margin: 20px;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#error_message {
|
||||
overflow: scroll;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
<script src="${vcdromJsUri}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="waveform-container">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载 VCD 波形...</p>
|
||||
</div>
|
||||
<div id="waveform1"></div>
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div id="error_container" style="display: none;">
|
||||
<h3>❌ Surfer 加载失败</h3>
|
||||
<code id="error_message"></code>
|
||||
</div>
|
||||
|
||||
<script src="${integrationJsUri}"></script>
|
||||
<script>
|
||||
(async function() {
|
||||
try {
|
||||
// 设置 WASM 文件路径
|
||||
window.wasmBinaryFile = '${vcdWasmUri}';
|
||||
register_message_listener();
|
||||
|
||||
// 解码 base64 VCD 内容
|
||||
const vcdBase64 = '${vcdBase64}';
|
||||
const vcdContent = atob(vcdBase64);
|
||||
console.log('[Webview] 注册 VS Code 消息监听器');
|
||||
// 监听来自 VS Code 扩展的消息(使用 vscode API)
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
// 隐藏加载提示
|
||||
document.querySelector('.loading').style.display = 'none';
|
||||
// 检查是否来自 VS Code
|
||||
if (message.command === 'loadVcdUrl') {
|
||||
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
|
||||
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
|
||||
|
||||
// 创建一个函数来提供 VCD 数据流
|
||||
const vcdProvider = async (handler) => {
|
||||
// 将 VCD 内容转换为 Uint8Array
|
||||
const encoder = new TextEncoder();
|
||||
const vcdData = encoder.encode(vcdContent);
|
||||
|
||||
// 创建一个 ReadableStream reader
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(vcdData);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
// 调用 handler 并传递 reader
|
||||
await handler([{
|
||||
key: 'local',
|
||||
value: 'waveform.vcd',
|
||||
format: 'raw',
|
||||
baseName: 'waveform.vcd',
|
||||
ext: 'vcd',
|
||||
reader: reader
|
||||
}]);
|
||||
};
|
||||
|
||||
// 初始化 VCDrom,使用函数回调方式
|
||||
if (typeof VCDrom === 'function') {
|
||||
await VCDrom('waveform1', vcdProvider);
|
||||
if (window.surferReady) {
|
||||
// Surfer 已就绪,立即加载
|
||||
loadVcdUrl(message);
|
||||
} else {
|
||||
throw new Error('VCDrom 未正确加载');
|
||||
// Surfer 未就绪,保存数据等待加载
|
||||
console.log('[Webview] Surfer 未就绪,保存数据待加载');
|
||||
window.pendingVcdData = message;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载 VCD 波形失败:', error);
|
||||
document.getElementById('waveform-container').innerHTML =
|
||||
'<div class="error-message">' +
|
||||
'<h3>❌ 加载 VCD 波形失败</h3>' +
|
||||
'<p>' + error.message + '</p>' +
|
||||
'<p style="margin-top: 10px;">请确保 VCD 文件格式正确。</p>' +
|
||||
'<pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); overflow: auto;">' + error.stack + '</pre>' +
|
||||
'</div>';
|
||||
}
|
||||
})();
|
||||
}, true); // 使用捕获阶段,优先于 integration.js 的监听器
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
145
src/services/vcdFileServer.ts
Normal file
145
src/services/vcdFileServer.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as http from "http";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
/**
|
||||
* VCD 文件 HTTP 服务器
|
||||
* 用于为 Surfer 波形查看器提供 VCD 文件访问
|
||||
*/
|
||||
export class VCDFileServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 0;
|
||||
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
public async start(): Promise<number> {
|
||||
if (this.server) {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// 监听随机端口
|
||||
this.server.listen(0, "127.0.0.1", () => {
|
||||
const address = this.server!.address();
|
||||
if (address && typeof address === "object") {
|
||||
this.port = address.port;
|
||||
console.log(`[VCDFileServer] 服务器已启动,端口: ${this.port}`);
|
||||
resolve(this.port);
|
||||
} else {
|
||||
reject(new Error("无法获取服务器端口"));
|
||||
}
|
||||
});
|
||||
|
||||
this.server.on("error", (error) => {
|
||||
console.error("[VCDFileServer] 服务器错误:", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
*/
|
||||
public stop(): void {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
this.port = 0;
|
||||
this.vcdFiles.clear();
|
||||
console.log("[VCDFileServer] 服务器已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 VCD 文件
|
||||
*/
|
||||
public registerFile(filePath: string): string {
|
||||
const fileId = this.generateFileId(filePath);
|
||||
this.vcdFiles.set(fileId, filePath);
|
||||
console.log(`[VCDFileServer] 注册文件: ${fileId} -> ${filePath}`);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件 URL
|
||||
*/
|
||||
public getFileUrl(fileId: string): string {
|
||||
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件 ID
|
||||
*/
|
||||
private generateFileId(filePath: string): string {
|
||||
const timestamp = Date.now();
|
||||
const fileName = path.basename(filePath);
|
||||
return `${timestamp}-${fileName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const url = req.url || "";
|
||||
console.log(`[VCDFileServer] 收到请求: ${url}`);
|
||||
|
||||
// 设置 CORS 头
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 URL,提取文件 ID
|
||||
const match = url.match(/^\/vcd\/(.+)$/);
|
||||
if (!match) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = match[1];
|
||||
const filePath = this.vcdFiles.get(fileId);
|
||||
|
||||
if (!filePath) {
|
||||
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[VCDFileServer] 文件不存在: ${filePath}`);
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("File Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取并发送文件
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Length": fileContent.length,
|
||||
});
|
||||
res.end(fileContent);
|
||||
console.log(`[VCDFileServer] 成功发送文件: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error(`[VCDFileServer] 读取文件失败:`, error);
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
* 对应后端 IC Coder Backend 的接口格式
|
||||
*/
|
||||
|
||||
import { CompactedMemory, CompactedMessage } from './memory';
|
||||
import { CompactedMemory, CompactedMessage } from "./memory";
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
@ -14,7 +14,7 @@ import { CompactedMemory, CompactedMessage } from './memory';
|
||||
* - agent: 智能体自主(默认)
|
||||
* - auto: 完全自动
|
||||
*/
|
||||
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||
export type RunMode = "plan" | "ask" | "agent" | "auto";
|
||||
|
||||
/**
|
||||
* 服务等级类型
|
||||
@ -23,7 +23,7 @@ export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||
* - max: 最大性能
|
||||
* - auto: 自动选择
|
||||
*/
|
||||
export type ServiceTier = 'lite' | 'syntaxic' | 'max' | 'auto';
|
||||
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
@ -52,26 +52,26 @@ export interface DialogRequest {
|
||||
|
||||
/** SSE 事件类型枚举 */
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_confirm' // 工具确认请求(Ask 模式)
|
||||
| 'plan_confirm' // 计划确认请求(Plan 模式)
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
| 'ask_user' // 向用户提问
|
||||
| 'agent_start' // 子智能体启动
|
||||
| 'agent_progress' // 子智能体进度
|
||||
| 'agent_complete' // 子智能体完成
|
||||
| 'agent_error' // 子智能体错误
|
||||
| 'memory_compacted' // 记忆压缩完成
|
||||
| 'context_usage' // 上下文使用量
|
||||
| 'complete' // 对话完成
|
||||
| 'error' // 错误
|
||||
| 'warning' // 警告
|
||||
| 'notification' // 通知
|
||||
| 'depth_update' // 深度更新
|
||||
| 'heartbeat'; // 心跳
|
||||
| "text_delta" // 文本增量
|
||||
| "tool_call" // 客户端工具调用请求
|
||||
| "tool_confirm" // 工具确认请求(Ask 模式)
|
||||
| "plan_confirm" // 计划确认请求(Plan 模式)
|
||||
| "tool_start" // 工具开始执行
|
||||
| "tool_complete" // 工具执行完成
|
||||
| "tool_error" // 工具执行错误
|
||||
| "ask_user" // 向用户提问
|
||||
| "agent_start" // 子智能体启动
|
||||
| "agent_progress" // 子智能体进度
|
||||
| "agent_complete" // 子智能体完成
|
||||
| "agent_error" // 子智能体错误
|
||||
| "memory_compacted" // 记忆压缩完成
|
||||
| "context_usage" // 上下文使用量
|
||||
| "complete" // 对话完成
|
||||
| "error" // 错误
|
||||
| "warning" // 警告
|
||||
| "notification" // 通知
|
||||
| "depth_update" // 深度更新
|
||||
| "heartbeat"; // 心跳
|
||||
|
||||
/** text_delta 事件数据 */
|
||||
export interface TextDeltaEvent {
|
||||
@ -173,7 +173,7 @@ export interface AgentProgressEvent {
|
||||
toolName: string;
|
||||
toolInput?: unknown;
|
||||
toolResult?: string;
|
||||
status: 'running' | 'completed' | 'error';
|
||||
status: "running" | "completed" | "error";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@ -209,11 +209,11 @@ export interface ContextUsageEvent {
|
||||
*/
|
||||
export interface ToolCallRequest {
|
||||
/** JSON-RPC版本,固定为"2.0" */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,用于匹配响应 */
|
||||
id: number;
|
||||
/** 方法名,固定为"tools/call" */
|
||||
method: 'tools/call';
|
||||
method: "tools/call";
|
||||
/** 调用参数 */
|
||||
params: {
|
||||
/** 工具名称 */
|
||||
@ -229,7 +229,7 @@ export interface ToolCallRequest {
|
||||
*/
|
||||
export interface ToolCallResult {
|
||||
/** JSON-RPC版本 */
|
||||
jsonrpc: '2.0';
|
||||
jsonrpc: "2.0";
|
||||
/** 请求ID,与ToolCallRequest.id对应 */
|
||||
id: number;
|
||||
/** 执行结果(与error互斥) */
|
||||
@ -314,16 +314,16 @@ export interface ToolConfirmResponse {
|
||||
|
||||
/** 后端工具名称 */
|
||||
export type ToolName =
|
||||
| 'file_read'
|
||||
| 'file_write'
|
||||
| 'file_delete'
|
||||
| 'file_list'
|
||||
| 'syntax_check'
|
||||
| 'simulation'
|
||||
| 'waveform_summary'
|
||||
| 'waveform_trace'
|
||||
| 'knowledge_save'
|
||||
| 'knowledge_load';
|
||||
| "file_read"
|
||||
| "file_write"
|
||||
| "file_delete"
|
||||
| "file_list"
|
||||
| "syntax_check"
|
||||
| "simulation"
|
||||
| "waveform_summary"
|
||||
| "waveform_trace"
|
||||
| "knowledge_save"
|
||||
| "knowledge_load";
|
||||
|
||||
/** file_read 工具参数 */
|
||||
export interface FileReadArgs {
|
||||
|
||||
@ -172,15 +172,8 @@ export function getAgentCardScript(): string {
|
||||
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
|
||||
const displayName = getAgentToolDisplayName(step.toolName);
|
||||
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
|
||||
// 为技术性工具调用添加低调样式(用户看不懂的)
|
||||
const lowProfileTools = [
|
||||
'knowledge_save', 'knowledge_load',
|
||||
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||
'setModule', 'addSignal', 'addSignalExample',
|
||||
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||
'showPlan', 'spawnExplorer'
|
||||
];
|
||||
const stepClass = lowProfileTools.includes(step.toolName) ? 'agent-step low-profile' : 'agent-step';
|
||||
// 所有工具调用都使用低调样式
|
||||
const stepClass = 'agent-step low-profile';
|
||||
return \`<div class="\${stepClass}"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
|
||||
}).join('');
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
import {
|
||||
getWaveformPreviewContent,
|
||||
@ -30,6 +31,10 @@ import {
|
||||
} from "./waveformPreviewContent";
|
||||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||||
import {
|
||||
getCodeHighlightStyles,
|
||||
getCodeHighlightScript,
|
||||
} from "../components/codeHighlight";
|
||||
|
||||
/**
|
||||
* 获取消息区域的 HTML 内容
|
||||
@ -294,7 +299,7 @@ export function getMessageAreaStyles(): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -303,71 +308,69 @@ export function getMessageAreaStyles(): string {
|
||||
/* Markdown 样式 */
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3 {
|
||||
margin: 16px 0 8px 0;
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1 {
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2 {
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3 {
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text pre {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.segment-text code {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.segment-text pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.segment-text code:not(pre code) {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol {
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong {
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em {
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a {
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover {
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p {
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
@ -375,7 +378,7 @@ export function getMessageAreaStyles(): string {
|
||||
}
|
||||
/* 低调显示的工具调用 - 移除边距和背景 */
|
||||
.segment-tool.low-profile {
|
||||
margin: 2px 0;
|
||||
margin: 2px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
@ -541,7 +544,7 @@ export function getMessageAreaStyles(): string {
|
||||
/* 低调显示的工具调用样式 */
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
@ -559,7 +562,7 @@ export function getMessageAreaStyles(): string {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
@ -607,6 +610,7 @@ export function getMessageAreaStyles(): string {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
@ -642,6 +646,8 @@ export function getMessageAreaStyles(): string {
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -663,6 +669,7 @@ export function getMessageAreaScript(): string {
|
||||
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||||
|
||||
${getAgentCardScript()}
|
||||
|
||||
@ -692,7 +699,8 @@ export function getMessageAreaScript(): string {
|
||||
'showPlan': searchCodeIconSvg,
|
||||
'addRule': fileWriteIconSvg,
|
||||
'updateNode': fileWriteIconSvg,
|
||||
'addStateTransition': stateTransitionIconSvg
|
||||
'addStateTransition': stateTransitionIconSvg,
|
||||
'askUser': userQuestionIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || '';
|
||||
}
|
||||
@ -722,7 +730,9 @@ export function getMessageAreaScript(): string {
|
||||
'addRule': '已添加规则',
|
||||
'updateNode': '已更新节点',
|
||||
'addStateTransition': '已添加状态转换',
|
||||
'spawnExplorer': '代码探索'
|
||||
'spawnExplorer': '代码探索',
|
||||
'spawnDebugger': '波形调试',
|
||||
'askUser': '用户提问',
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
@ -919,6 +929,7 @@ export function getMessageAreaScript(): string {
|
||||
// 实时更新分段消息(按后端返回顺序)
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
@ -995,17 +1006,8 @@ export function getMessageAreaScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 为技术性工具调用添加低调样式
|
||||
const lowProfileTools = [
|
||||
'knowledge_save', 'knowledge_load',
|
||||
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||
'setModule', 'addSignal', 'addSignalExample',
|
||||
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
|
||||
];
|
||||
if (lowProfileTools.includes(segment.toolName)) {
|
||||
segmentDiv.className += ' low-profile';
|
||||
}
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
@ -1098,7 +1100,7 @@ export function getMessageAreaScript(): string {
|
||||
: '';
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-text">\${segment.question || ''}</div>
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||||
@ -1257,17 +1259,8 @@ export function getMessageAreaScript(): string {
|
||||
return;
|
||||
}
|
||||
|
||||
// 为技术性工具调用添加低调样式
|
||||
const lowProfileTools = [
|
||||
'knowledge_save', 'knowledge_load',
|
||||
'queryKnowledgeSummary', 'queryRules', 'querySignals',
|
||||
'setModule', 'addSignal', 'addSignalExample',
|
||||
'validateKnowledgeGraph', 'addPlan', 'addEdge',
|
||||
'showPlan', 'addRule', 'updateNode', 'addStateTransition'
|
||||
];
|
||||
if (lowProfileTools.includes(segment.toolName)) {
|
||||
segmentDiv.className += ' low-profile';
|
||||
}
|
||||
// 所有工具调用都使用低调样式
|
||||
segmentDiv.className += ' low-profile';
|
||||
|
||||
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||||
const toolResult = segment.toolResult || '';
|
||||
@ -1335,7 +1328,7 @@ export function getMessageAreaScript(): string {
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="question-segment">
|
||||
<div class="question-text">\${segment.question || ''}</div>
|
||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||||
<div class="question-options">
|
||||
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
|
||||
</div>
|
||||
@ -1393,21 +1386,41 @@ export function getMessageAreaScript(): string {
|
||||
function formatText(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// 先转义 HTML 特殊字符
|
||||
let html = text
|
||||
let html = text;
|
||||
|
||||
// 先提取并处理代码块(避免被转义)
|
||||
const codeBlocks = [];
|
||||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
const language = lang || 'plaintext';
|
||||
// 转义代码内容
|
||||
const escapedCode = code.trim()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
// 不再手动高亮,让 highlight.js 处理
|
||||
const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
|
||||
codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 提取行内代码(避免被转义)
|
||||
const inlineCodes = [];
|
||||
html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
|
||||
const escapedCode = code
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
|
||||
inlineCodes.push('<code>' + escapedCode + '</code>');
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// 转义其他 HTML 特殊字符
|
||||
html = html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 处理代码块(三个反引号包裹的代码)
|
||||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
const language = lang || 'plaintext';
|
||||
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
|
||||
});
|
||||
|
||||
// 处理行内代码(单个反引号包裹)
|
||||
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
||||
|
||||
// 处理标题 ### Title
|
||||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||||
@ -1429,9 +1442,19 @@ export function getMessageAreaScript(): string {
|
||||
// 处理链接 [text](url)
|
||||
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// 处理换行
|
||||
// 处理换行(在恢复代码块之前)
|
||||
html = html.replace(/\\n/g, '<br>');
|
||||
|
||||
// 恢复代码块(在最后恢复,避免被其他处理影响)
|
||||
codeBlocks.forEach((block, index) => {
|
||||
html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
|
||||
});
|
||||
|
||||
// 恢复行内代码
|
||||
inlineCodes.forEach((code, index) => {
|
||||
html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1581,5 +1604,7 @@ export function getMessageAreaScript(): string {
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -5,65 +5,79 @@ export function getWaveformPreviewContent(): string {
|
||||
return `
|
||||
/* 波形预览组件样式 */
|
||||
.waveform-preview {
|
||||
margin-top: 12px;
|
||||
margin: 16px 0;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
.waveform-preview:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.waveform-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, var(--vscode-input-background) 0%, var(--vscode-editor-background) 100%);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.waveform-preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.waveform-preview-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--vscode-button-background);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
.waveform-expand-btn {
|
||||
padding: 4px 12px;
|
||||
padding: 6px 14px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: opacity 0.2s ease;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.waveform-expand-btn:hover {
|
||||
opacity: 0.9;
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.waveform-expand-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.waveform-expand-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.waveform-preview-content {
|
||||
padding: 0;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.waveform-preview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
height: auto;
|
||||
}
|
||||
.waveform-preview-placeholder {
|
||||
display: flex;
|
||||
@ -88,7 +102,8 @@ export function getWaveformPreviewContent(): string {
|
||||
}
|
||||
.waveform-mini-viewer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: auto;
|
||||
min-height: 120px;
|
||||
background: var(--vscode-editor-background);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
getProgressBarStyles,
|
||||
getProgressBarScript,
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
/**
|
||||
* 获取 WebView 面板的 HTML 内容
|
||||
@ -44,6 +45,7 @@ export function getWebviewContent(
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IC Coder</title>
|
||||
${getHighlightJsLinks()}
|
||||
<style>
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
@ -269,7 +271,7 @@ export function getWebviewContent(
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -312,7 +314,7 @@ export function getWebviewContent(
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.question-segment .question-text {
|
||||
|
||||
Reference in New Issue
Block a user