diff --git a/vscode-wpilib/media/main.css b/vscode-wpilib/media/main.css index 04a5df37..2287cfe4 100644 --- a/vscode-wpilib/media/main.css +++ b/vscode-wpilib/media/main.css @@ -1,15 +1,18 @@ .installed-dependency, .available-dependency { margin-bottom: 10px; + &:first-of-type { margin-top: 10px; } } + .top-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; + &:has(#updateall-action) { margin-top: 4px; } @@ -18,36 +21,928 @@ #updateall-action { width: 100%; } + .name { font-weight: 600; } + .downloads { display: flex; align-items: center; } + .icon { margin-left: 5px; } + .details { margin-top: 5px; color: var(--vscode-descriptionForeground); } + .update { display: flex; gap: 4px; } + .uninstall-button { - padding: 4px; - & > vscode-icon { + padding: 4px !important; + + & > i { margin-right: 4px !important; margin-left: 4px !important; } } -vscode-collapsible::part(body) { +.vscode-collapsible div { overflow: visible; } -vscode-single-select { +.vscode-select select, +.vscode-select { flex-shrink: 2; } + +button[id*='install-action'] { + flex-shrink: 0; +} + +button[id*='version-action'] { + overflow: visible; + padding: 1px 6px; +} + +html { + scrollbar-gutter: stable; +} + +/* select styles */ +.vscode-select { + position: relative; + width: 320px; +} + +.vscode-select select { + appearance: none; + background-color: var(--vscode-settings-dropdownBackground); + border-color: var(--vscode-settings-dropdownBorder); + border-radius: 2px; + border-style: solid; + border-width: 1px; + color: var(--vscode-settings-dropdownForeground); + cursor: pointer; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + line-height: 18px; + margin: 0; + padding: 3px 4px; + width: 100%; +} + +.vscode-select select:focus { + border-color: var(--vscode-focusBorder); + outline: none; +} + +.vscode-select.invalid select, +.vscode-select select:invalid { + background-color: var(--vscode-inputValidation-errorBackground); + border-color: var(--vscode-inputValidation-errorBorder, #be1100); +} + +.vscode-select option { + cursor: pointer; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); +} + +.vscode-select .chevron-icon { + color: var(--vscode-foreground); + display: block; + height: 14px; + pointer-events: none; + position: absolute; + right: 8px; + top: 5px; + width: 14px; + transform: rotate(90deg); +} + +.vscode-select select[multiple] { + padding: 0; +} + +.vscode-select select[multiple] + .chevron-icon { + display: none; +} + +.vscode-select select[multiple] option { + color: var(--vscode-foreground); + cursor: pointer; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + line-height: 18px; + padding: 2px 3px; +} + +.vscode-select select[multiple] option:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); +} + +.vscode-select select[multiple] option:checked { + background-color: var(--vscode-list-hoverBackground) !important; + color: var(--vscode-list-hoverForeground) !important; +} + +.vscode-select select[multiple]:focus option:checked { + background-color: var(--vscode-list-activeSelectionBackground) !important; + color: var(--vscode-list-activeSelectionForeground) !important; +} + +/* button styles */ +.vscode-button { + align-items: center; + background-color: var(--vscode-button-background); + border-color: var(--vscode-button-border, var(--vscode-button-background)); + border-style: solid; + border-radius: 2px; + border-width: 1px; + color: var(--vscode-button-foreground); + cursor: pointer; + display: inline-flex; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + line-height: 22px; + overflow: hidden; + padding: 1px 13px; + user-select: none; + white-space: nowrap; +} + +.vscode-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.vscode-button:focus, +.vscode-button:active { + outline: none; +} + +.vscode-button:focus { + background-color: var(--vscode-button-hoverBackground); + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + +.vscode-button.secondary { + color: var(--vscode-button-secondaryForeground); + background-color: var(--vscode-button-secondaryBackground); + border-color: var(--vscode-button-border, var(--vscode-button-secondaryBackground)); +} + +.vscode-button.secondary:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.vscode-button.secondary:focus { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.vscode-button:disabled { + cursor: default; + opacity: 0.4; + pointer-events: none; +} + +.vscode-button:disabled:hover { + background-color: var(--vscode-button-background); +} + +.vscode-button:disabled { + background-color: var(--vscode-button-background); + outline: 0; +} + +.vscode-button.secondary:disabled { + background-color: var(--vscode-button-secondaryBackground); +} + +.vscode-button.secondary:disabled:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.vscode-button.block { + align-items: center; + display: flex; + justify-content: center; + padding-bottom: 2px; + padding-top: 2px; + width: 100%; +} + +.vscode-button svg { + display: block; +} + +.vscode-button .codicon, +.vscode-button svg { + margin-left: 3px; + margin-right: 3px; +} + +.vscode-button .codicon:first-child, +.vscode-button svg:first-child { + margin-left: 0; +} + +.vscode-button .codicon:last-child, +.vscode-button svg:last-child { + margin-right: 0; +} + +/* collapsible */ + +.vscode-collapsible + .vscode-collapsible { + border-top: 1px solid var(--vscode-sideBarSectionHeader-border); +} + +.vscode-collapsible summary { + align-items: center; + background-color: var(--vscode-sideBarSectionHeader-background); + cursor: pointer; + display: flex; + height: 22px; + line-height: 22px; + user-select: none; +} + +.vscode-collapsible summary:focus { + opacity: 1; + outline-offset: -1px; + outline-style: solid; + outline-width: 1px; + outline-color: var(--vscode-focusBorder); +} + +.vscode-collapsible .icon-arrow { + display: block; + margin: 0 3px; +} + +.vscode-collapsible[open] .icon-arrow { + transform: rotate(90deg); +} + +.vscode-collapsible .title { + color: var(--vscode-sideBarSectionHeader-foreground); + display: block; + font-family: var(--vscode-font-family); + font-size: 11px; + font-weight: 700; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.vscode-collapsible .title .description { + display: none; + font-weight: 400; + margin-left: 10px; + text-transform: none; + opacity: 0.6; +} + +.vscode-collapsible[open] .title .description { + display: inline; +} + +.vscode-collapsible .actions { + align-items: center; + height: 22px; + margin-left: auto; + margin-right: 4px; +} + +.vscode-badge { + background-color: var(--vscode-badge-background); + border: 1px solid var(--vscode-contrastBorder, transparent); + border-radius: 2px; + box-sizing: border-box; + color: var(--vscode-badge-foreground); + display: inline-block; + font-family: var(--vscode-font-family); + font-size: 11px; + font-weight: 400; + line-height: 14px; + min-width: 18px; + padding: 2px 3px; + text-align: center; + white-space: nowrap; +} + +.vscode-badge.counter { + border-radius: 11px; + box-sizing: border-box; + height: 18px; + line-height: 1; + padding: 3px 5px; +} + +.vscode-badge.activity-bar-counter { + background-color: var(--vscode-activityBarBadge-background); + border-radius: 20px; + color: var(--vscode-activityBarBadge-foreground); + font-size: 9px; + font-weight: 600; + line-height: 16px; + padding: 0 4px; +} + +/* RioLog Styles */ +.toolbar { + background-color: var(--vscode-editor-background); + border-top: 1px solid var(--vscode-panel-border); + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 100; +} + +.status-container { + display: flex; + align-items: center; + margin-bottom: 4px; +} + +.connection-status { + width: 16px; + height: 16px; + border-radius: 50%; + margin-right: 8px; +} + +.connection-status.connected { + background-color: var(--vscode-testing-iconPassed, #4caf50); + box-shadow: 0 0 5px var(--vscode-testing-iconPassed, #4caf50); +} + +.connection-status.disconnected { + background-color: var(--vscode-testing-iconFailed, #f44336); + box-shadow: 0 0 5px var(--vscode-testing-iconFailed, #f44336); +} + +.team-number-container { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.team-number-input { + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, var(--vscode-focusBorder)); + color: var(--vscode-input-foreground); + padding: 4px 8px; + width: 80px; + border-radius: 4px; +} + +.team-number-input.error { + border-color: var(--vscode-inputValidation-errorBorder); + background-color: var(--vscode-inputValidation-errorBackground); +} + +.search-container { + margin-bottom: 8px; +} + +.search-input { + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, var(--vscode-focusBorder)); + color: var(--vscode-input-foreground); + padding: 6px 8px; + width: 100%; + border-radius: 4px; + box-sizing: border-box; +} + +.buttons-container { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.button-group { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-right: 6px; +} + +.toolbar-button { + border: none; + padding: 6px 8px; + text-align: center; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border-radius: 4px; + font-size: 12px; + cursor: pointer; +} + +.toolbar-button:hover { + background: var(--vscode-button-hoverBackground); +} + +.toolbar-button:focus { + outline-color: var(--vscode-focusBorder); +} + +.toolbar-button.active { + background-color: var(--vscode-button-secondaryBackground, var(--vscode-button-background)); + color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); +} + +.toolbar-button.success { + background-color: var(--vscode-testing-iconPassed, #4caf50); +} + +#log-container { + padding: 8px; + overflow-y: auto; + max-height: calc(100vh - 60px); + font-family: var(--vscode-editor-font-family, monospace); + font-size: var(--vscode-editor-font-size, 13px); + line-height: 1.4; +} + +.log-entry { + margin-bottom: 4px; + padding: 2px 4px; + border-radius: 2px; + position: relative; +} + +.log-entry:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.error-log { + border-left: 3px solid var(--vscode-testing-iconFailed, #f44336); +} + +.warning-log { + border-left: 3px solid var(--vscode-warningForeground, #ff9800); +} + +.print-log { + border-left: 3px solid transparent; +} + +.log-message { + display: flex; + align-items: flex-start; +} + +.timestamp { + color: var(--vscode-descriptionForeground); + margin-right: 8px; + font-size: 12px; + user-select: none; +} + +.message-content { + flex: 1; + word-break: break-word; +} + +.toggle-button { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 6px; + cursor: pointer; + position: relative; + top: 2px; +} + +.toggle-button.collapsed::before { + content: '►'; + font-size: 10px; + color: var(--vscode-descriptionForeground); +} + +.toggle-button.expanded::before { + content: '▼'; + font-size: 10px; + color: var(--vscode-descriptionForeground); +} + +.error-content { + margin-left: 12px; +} + +/* Rules to properly handle error collapsing in RioLog */ +.error-content.collapsed .stack-trace, +.error-content.collapsed .location-info { + display: none; +} + +.stack-trace, +.location-info { + margin-top: 4px; + margin-left: 12px; + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +/* Handle input styling for number inputs */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='number'] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* File input styling for the log viewer mode */ +input[type='file'] { + display: none; +} + +label.toolbar-button { + display: inline-block; + cursor: pointer; +} + +/* Project Creator and Import Styles */ +.project-container { + display: flex; + flex-direction: column; + margin: 0 auto; + padding: 16px; + max-width: 800px; +} + +.project-row { + display: flex; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 12px; +} + +.project-label { + flex: 0 0 140px; + font-weight: 500; +} + +.project-input { + flex: 1; + min-width: 250px; + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, var(--vscode-focusBorder)); + color: var(--vscode-input-foreground); + padding: 6px 8px; + border-radius: 4px; +} + +.project-input.error { + border-color: var(--vscode-inputValidation-errorBorder); + background-color: var(--vscode-inputValidation-errorBackground); +} + +.project-button { + border: none; + padding: 6px 12px; + text-align: center; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border-radius: 4px; + font-size: 12px; + cursor: pointer; +} + +.project-button:hover { + background: var(--vscode-button-hoverBackground); +} + +.project-button:focus { + outline-color: var(--vscode-focusBorder); +} + +.project-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.project-select { + min-width: 250px; + background-color: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + color: var(--vscode-dropdown-foreground); + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + width: 100%; +} + +.project-checkbox-container { + display: flex; + align-items: center; + margin-right: 16px; + user-select: none; + flex-wrap: wrap; +} + +.project-checkbox { + margin-right: 8px; +} + +.project-actions { + display: flex; + justify-content: flex-end; + margin-top: 24px; + gap: 12px; +} + +.project-title { + font-size: 20px; + margin-bottom: 24px; + color: var(--vscode-foreground); + font-weight: 500; +} + +.project-subtitle { + font-size: 16px; + margin-bottom: 12px; + color: var(--vscode-foreground); + width: 100%; +} + +.project-error-text { + color: var(--vscode-testing-iconFailed, #f44336); + font-size: 12px; + margin-top: 4px; + display: none; +} + +.project-checkboxes { + display: flex; + flex-wrap: wrap; + margin-bottom: 16px; +} + +/* Wizard Styles */ +.wizard-progress { + display: flex; + margin-bottom: 24px; + position: relative; + justify-content: space-between; +} + +.wizard-progress::before { + content: ''; + position: absolute; + top: 15px; + left: 0; + right: 0; + height: 2px; + background: var(--vscode-editor-lineHighlightBorder, rgba(255, 255, 255, 0.1)); + z-index: 1; +} + +.progress-step { + position: relative; + padding: 0 10px; + font-size: 14px; + text-align: center; + color: var(--vscode-disabledForeground); + z-index: 2; + background-color: var(--vscode-editor-background); + font-weight: 500; +} + +.progress-step::before { + content: attr(data-step); + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + margin: 0 auto 8px; + background-color: var(--vscode-editor-lineHighlightBackground, rgba(255, 255, 255, 0.05)); + border: 1px solid var(--vscode-editor-lineHighlightBorder, rgba(255, 255, 255, 0.1)); + color: var(--vscode-disabledForeground); +} + +.progress-step.active { + color: var(--vscode-foreground); +} + +.progress-step.active::before { + background-color: var(--vscode-button-background); + border-color: var(--vscode-button-background); + color: var (--vscode-button-foreground); +} + +.progress-step.completed::before { + content: '✓'; + background-color: var(--vscode-testing-iconPassed, #4caf50); + border-color: var(--vscode-testing-iconPassed, #4caf50); + color: var(--vscode-button-foreground); +} + +.wizard-step { + display: none; +} + +.wizard-step.active { + display: block; +} + +.wizard-navigation { + display: flex; + justify-content: space-between; + margin-top: 24px; + border-top: 1px solid var(--vscode-panel-border); + padding-top: 16px; +} + +.next-button, +.primary-button { + background-color: var(--vscode-button-prominentBackground, var(--vscode-button-background)); + color: var(--vscode-button-prominentForeground, var(--vscode-button-foreground)); +} + +.back-button { + background-color: var(--vscode-button-secondaryBackground, transparent); + color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); + border: 1px solid var(--vscode-button-border, var(--vscode-panel-border)); +} + +.step-header { + margin-bottom: 24px; +} + +.step-header h2 { + margin-bottom: 8px; + font-size: 18px; + font-weight: 500; +} + +.step-header p { + color: var(--vscode-descriptionForeground); + margin: 0; +} + +.selection-cards { + display: flex; + gap: 16px; + margin: 24px 0; +} + +.selection-card { + flex: 1; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 16px; + cursor: pointer; + text-align: center; + position: relative; + transition: all 0.2s; +} + +.selection-card:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.selection-card.selected { + border-color: var(--vscode-button-background); + background-color: var(--vscode-editor-lineHighlightBackground, rgba(255, 255, 255, 0.05)); +} + +.selection-card h3 { + margin-top: 0; + margin-bottom: 8px; +} + +.selection-card p { + color: var(--vscode-descriptionForeground); + font-size: 13px; + margin-bottom: 16px; +} + +.card-icon { + color: var(--vscode-descriptionForeground); +} + +.selection-card.selected .card-icon { + color: var(--vscode-button-background); +} + +.select-wrapper { + position: relative; + flex: 1; +} + +.project-field-container { + display: flex; + flex-direction: column; + flex: 1; +} + +.checkbox-help { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-left: 8px; +} + +.summary-box { + width: 100%; + padding: 16px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-editor-lineHighlightBackground, rgba(255, 255, 255, 0.05)); +} + +.summary-row { + margin-bottom: 8px; +} + +.summary-row:last-child { + margin-bottom: 0; +} + +.import-note { + width: 100%; + padding: 12px; + border-left: 4px solid var(--vscode-textLink-activeForeground); + background-color: var(--vscode-editorHoverWidget-background, rgba(255, 255, 255, 0.05)); + border-radius: 2px; +} + +.import-note p { + margin: 8px 0; +} + +.import-note a { + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +.import-note a:hover { + text-decoration: underline; +} + +/* Add basic styling for form elements to ensure they appear regardless of project-* classes */ +button { + font-size: 13px; + padding: 4px 8px; + margin: 0; + cursor: pointer; + background-color: var(--vscode-button-background, #0e639c); + color: var(--vscode-button-foreground, white); + border: none; + border-radius: 2px; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +input[type='text'], +input[type='number'] { + background-color: var(--vscode-input-background, white); + color: var(--vscode-input-foreground, black); + border: 1px solid var(--vscode-input-border, #ccc); + padding: 4px; + font-size: 13px; +} + +input.error { + border-color: var(--vscode-inputValidation-errorBorder, red) !important; + outline-color: var(--vscode-inputValidation-errorBorder, red) !important; +} + +.error-content.collapsed .stack-trace, +.error-content.collapsed .location-info { + display: none; +} diff --git a/vscode-wpilib/resources/testing/riosender.py b/vscode-wpilib/resources/testing/riosender.py index ca4e7efd..756af9de 100644 --- a/vscode-wpilib/resources/testing/riosender.py +++ b/vscode-wpilib/resources/testing/riosender.py @@ -47,7 +47,7 @@ def handle(self): while 1: timestamp = time.time() - startTime sequence = (sequence + 1) & 0xffff - split = self.makeErrorMsgSplit(timestamp, sequence, 1, 0x111111, 1, "this is an
errorwitha\r\nong text", "foo.c:1111", "traceback 1\ntraceback 2\ntraceback 3\n") + split = self.makeErrorMsgSplit(timestamp, sequence, 1, 0x111111, 1, "this is an error with a super long text. We want to know if the riolog will correctly wrap the content to the next line once it goes wayyyyyyyyyyyyyyyyyyyyyyy over the length limit.", "foo.c:1111", "traceback 1\ntraceback 2\ntraceback 3\n") print(split[0]) print (split[1]) self.wfile.write(split[0]) diff --git a/vscode-wpilib/src/riolog/ansi/ansiparser.ts b/vscode-wpilib/src/riolog/ansi/ansiparser.ts new file mode 100644 index 00000000..cf0c810a --- /dev/null +++ b/vscode-wpilib/src/riolog/ansi/ansiparser.ts @@ -0,0 +1,212 @@ +'use strict'; + +// ANSI color codes +export interface AnsiState { + foreground?: string; + background?: string; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + dim?: boolean; +} + +// Color mapping for standard ANSI colors +const foregroundColors: { [key: number]: string } = { + 30: '#000000', // black + 31: '#f44336', // red + 32: '#4caf50', // green + 33: '#ffeb3b', // yellow + 34: '#2196f3', // blue + 35: '#e91e63', // magenta + 36: '#00bcd4', // cyan + 37: '#ffffff', // white + 90: '#9e9e9e', // gray + 91: '#ff5252', // lightred + 92: '#8bc34a', // lightgreen + 93: '#ffc107', // lightyellow + 94: '#03a9f4', // lightblue + 95: '#ec407a', // lightmagenta + 96: '#26c6da', // lightcyan + 97: '#fafafa', // white +}; + +const backgroundColors: { [key: number]: string } = { + 40: '#000000', // black + 41: '#f44336', // red + 42: '#4caf50', // green + 43: '#ffeb3b', // yellow + 44: '#2196f3', // blue + 45: '#e91e63', // magenta + 46: '#00bcd4', // cyan + 47: '#ffffff', // white + 100: '#9e9e9e', // gray + 101: '#ff5252', // lightred + 102: '#8bc34a', // lightgreen + 103: '#ffc107', // lightyellow + 104: '#03a9f4', // lightblue + 105: '#ec407a', // lightmagenta + 106: '#26c6da', // lightcyan + 107: '#fafafa', // white +}; + +export interface AnsiSegment { + text: string; + state: AnsiState; +} + +// Parses a text string containing ANSI escape sequences and returns a collection of text segments with styling +export function parseAnsiString(text: string): AnsiSegment[] { + const result: AnsiSegment[] = []; + const regex = /\u001b\[((?:\d+;)*\d*)m/g; + + let lastIndex = 0; + let match: RegExpExecArray | null; + let currentState: AnsiState = {}; + + while ((match = regex.exec(text)) !== null) { + // Add text before the escape sequence with current state + const segment = text.substring(lastIndex, match.index); + if (segment) { + result.push({ + text: segment, + state: { ...currentState }, + }); + } + + // Update state based on escape sequence + const codes = match[1].split(';').map((num) => parseInt(num || '0', 10)); + + // Process codes + for (let i = 0; i < codes.length; i++) { + const code = codes[i]; + + // Process reset and text styles + if (code === 0) { + // Reset all attributes + currentState = {}; + } else if (code === 1) { + currentState.bold = true; + } else if (code === 2) { + currentState.dim = true; + } else if (code === 3) { + currentState.italic = true; + } else if (code === 4) { + currentState.underline = true; + } else if (code === 9) { + currentState.strikethrough = true; + } else if (code === 22) { + currentState.bold = false; + currentState.dim = false; + } else if (code === 23) { + currentState.italic = false; + } else if (code === 24) { + currentState.underline = false; + } else if (code === 29) { + currentState.strikethrough = false; + } else if (code === 39) { + delete currentState.foreground; + } else if (code === 49) { + delete currentState.background; + } + // Standard colors + else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) { + currentState.foreground = foregroundColors[code]; + } else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) { + currentState.background = backgroundColors[code]; + } + // 8-bit color support (256 colors) + else if (code === 38 && i + 2 < codes.length && codes[i + 1] === 5) { + const colorCode = codes[i + 2]; + // Generate 8-bit color + if (colorCode < 8) { + // Standard colors (0-7) + currentState.foreground = foregroundColors[colorCode + 30]; + } else if (colorCode < 16) { + // High intensity colors (8-15) + currentState.foreground = foregroundColors[colorCode - 8 + 90]; + } else if (colorCode < 232) { + // 216 colors (16-231): 6×6×6 cube + const r = Math.floor((colorCode - 16) / 36) * 51; + const g = Math.floor(((colorCode - 16) % 36) / 6) * 51; + const b = ((colorCode - 16) % 6) * 51; + currentState.foreground = `rgb(${r}, ${g}, ${b})`; + } else { + // Grayscale (232-255) + const gray = (colorCode - 232) * 10 + 8; + currentState.foreground = `rgb(${gray}, ${gray}, ${gray})`; + } + i += 2; // Skip the next two parameters + } else if (code === 48 && i + 2 < codes.length && codes[i + 1] === 5) { + const colorCode = codes[i + 2]; + // Same logic for background colors + if (colorCode < 8) { + currentState.background = backgroundColors[colorCode + 40]; + } else if (colorCode < 16) { + currentState.background = backgroundColors[colorCode - 8 + 100]; + } else if (colorCode < 232) { + const r = Math.floor((colorCode - 16) / 36) * 51; + const g = Math.floor(((colorCode - 16) % 36) / 6) * 51; + const b = ((colorCode - 16) % 6) * 51; + currentState.background = `rgb(${r}, ${g}, ${b})`; + } else { + const gray = (colorCode - 232) * 10 + 8; + currentState.background = `rgb(${gray}, ${gray}, ${gray})`; + } + i += 2; // Skip the next two parameters + } + // 24-bit color support (RGB) + else if (code === 38 && i + 4 < codes.length && codes[i + 1] === 2) { + const r = codes[i + 2]; + const g = codes[i + 3]; + const b = codes[i + 4]; + currentState.foreground = `rgb(${r}, ${g}, ${b})`; + i += 4; // Skip the next four parameters + } else if (code === 48 && i + 4 < codes.length && codes[i + 1] === 2) { + const r = codes[i + 2]; + const g = codes[i + 3]; + const b = codes[i + 4]; + currentState.background = `rgb(${r}, ${g}, ${b})`; + i += 4; // Skip the next four parameters + } + } + + lastIndex = match.index + match[0].length; + } + + // Add the remaining text with current state + const remainingText = text.substring(lastIndex); + if (remainingText) { + result.push({ + text: remainingText, + state: { ...currentState }, + }); + } + + return result; +} + +// Helper to apply ANSI styling to an HTML element +export function applyAnsiStyling(element: HTMLElement, state: AnsiState): void { + if (state.foreground) { + element.style.color = state.foreground; + } + if (state.background) { + element.style.backgroundColor = state.background; + } + if (state.bold) { + element.style.fontWeight = 'bold'; + } + if (state.dim) { + element.style.opacity = '0.7'; + } + if (state.italic) { + element.style.fontStyle = 'italic'; + } + if (state.underline) { + element.style.textDecoration = (element.style.textDecoration || '') + ' underline'; + } + if (state.strikethrough) { + element.style.textDecoration = (element.style.textDecoration || '') + ' line-through'; + } +} diff --git a/vscode-wpilib/src/riolog/promisecond.ts b/vscode-wpilib/src/riolog/promisecond.ts index e235c293..44c11ac2 100644 --- a/vscode-wpilib/src/riolog/promisecond.ts +++ b/vscode-wpilib/src/riolog/promisecond.ts @@ -9,7 +9,7 @@ export class PromiseCondition { this.condSet = () => { resolve(); }; - if (this.hasBeenSet === true) { + if (this.hasBeenSet) { resolve(); } }); @@ -17,7 +17,7 @@ export class PromiseCondition { public set() { this.hasBeenSet = true; - if (this.condSet !== undefined) { + if (this.condSet) { this.condSet(); } } diff --git a/vscode-wpilib/src/riolog/rioconnector.ts b/vscode-wpilib/src/riolog/rioconnector.ts index 2e5e39c8..40d6b80f 100644 --- a/vscode-wpilib/src/riolog/rioconnector.ts +++ b/vscode-wpilib/src/riolog/rioconnector.ts @@ -58,9 +58,7 @@ function timerPromise(ms: number): ICancellableTimer { let timer: NodeJS.Timeout | undefined; return { promise: new Promise((resolve, _) => { - timer = setTimeout(() => { - resolve(undefined); - }, ms); + timer = setTimeout(() => resolve(undefined), ms); }), cancel() { if (timer === undefined) { diff --git a/vscode-wpilib/src/riolog/riologwindow.ts b/vscode-wpilib/src/riolog/riologwindow.ts index 0a5ecf6f..41e98d09 100644 --- a/vscode-wpilib/src/riolog/riologwindow.ts +++ b/vscode-wpilib/src/riolog/riologwindow.ts @@ -39,7 +39,7 @@ export class RioLogWindow { return; } this.webview.on('didDispose', () => { - if (this.rioConsole !== undefined) { + if (this.rioConsole) { this.rioConsole.stop(); this.rioConsole.removeAllListeners(); } @@ -65,7 +65,7 @@ export class RioLogWindow { } public stop() { - if (this.webview !== undefined) { + if (this.webview) { this.webview.dispose(); } } @@ -79,27 +79,22 @@ export class RioLogWindow { private createWebView() { this.webview = this.windowProvider.createWindowView(); + this.webview.on('windowActive', async () => { if (this.webview === undefined) { return; } + // Window goes active. await this.webview.postMessage({ message: this.hiddenArray, type: SendTypes.Batch, }); - if (this.rioConsole !== undefined) { - if (this.rioConsole.connected === true) { - await this.webview.postMessage({ - message: true, - type: SendTypes.ConnectionChanged, - }); - } else { - await this.webview.postMessage({ - message: false, - type: SendTypes.ConnectionChanged, - }); - } + if (this.rioConsole) { + await this.webview.postMessage({ + message: true, + type: SendTypes.ConnectionChanged, + }); } }); } @@ -126,24 +121,17 @@ export class RioLogWindow { if (this.webview === undefined) { return; } - if (connected) { - await this.webview.postMessage({ - message: true, - type: SendTypes.ConnectionChanged, - }); - } else { - await this.webview.postMessage({ - message: false, - type: SendTypes.ConnectionChanged, - }); - } + await this.webview.postMessage({ + message: connected, + type: SendTypes.ConnectionChanged, + }); } private async onNewMessageToSend(message: IPrintMessage | IErrorMessage) { if (this.webview === undefined) { return; } - if (this.paused === true) { + if (this.paused) { this.pausedArray.push(message); await this.webview.postMessage({ message: this.pausedArray.length, @@ -169,7 +157,7 @@ export class RioLogWindow { } else if (data.type === ReceiveTypes.Pause) { const old = this.paused; this.paused = data.message as boolean; - if (old === true && this.paused === false) { + if (old && !this.paused) { await this.sendPaused(); } } else if (data.type === ReceiveTypes.Save) { @@ -185,7 +173,7 @@ export class RioLogWindow { } else if (data.type === ReceiveTypes.Reconnect) { const newValue = data.message as boolean; this.rioConsole.setAutoReconnect(newValue); - if (newValue === false) { + if (!newValue) { this.rioConsole.disconnect(); } } else if (data.type === ReceiveTypes.ChangeNumber) { diff --git a/vscode-wpilib/src/riolog/script/implscript.ts b/vscode-wpilib/src/riolog/script/implscript.ts index 1d79fc12..d7768b41 100644 --- a/vscode-wpilib/src/riolog/script/implscript.ts +++ b/vscode-wpilib/src/riolog/script/implscript.ts @@ -1,7 +1,7 @@ 'use strict'; import { IIPCReceiveMessage, IIPCSendMessage } from '../shared/interfaces'; -import { checkResizeImpl, handleMessage } from '../shared/sharedscript'; +import { setImplFunctions, handleMessage } from '../shared/sharedscript'; interface IVsCodeApi { postMessage(message: IIPCReceiveMessage, to: string): void; @@ -11,19 +11,40 @@ declare function acquireVsCodeApi(): IVsCodeApi; const vscode = acquireVsCodeApi(); -export function checkResize() { - checkResizeImpl(document.documentElement); +export function checkResize(): void { + // Get required elements + const toolbar = document.getElementById('toolbar'); + const logContainer = document.getElementById('log-container'); + + // Apply dynamic max-height calculation if both elements exist + if (toolbar && logContainer) { + logContainer.style.maxHeight = `calc(100vh - ${toolbar.offsetHeight}px)`; + } } export function scrollImpl() { - document.documentElement.scrollTop = document.documentElement.scrollHeight; + const logContainer = document.getElementById('log-container'); + if (logContainer) { + logContainer.scrollTop = logContainer.scrollHeight; + } } export function sendMessage(message: IIPCReceiveMessage) { vscode.postMessage(message, '*'); } +// Register the implementation functions with the shared module +console.log('Setting impl functions'); +setImplFunctions(checkResize, scrollImpl, sendMessage); + window.addEventListener('message', (event) => { - const data: IIPCSendMessage = event.data as IIPCSendMessage; + const data: IIPCSendMessage | any = event.data; handleMessage(data); + checkResize(); }); + +// Listen for window resize events +window.addEventListener('resize', checkResize); + +// Initialize everything once loaded +window.addEventListener('load', () => setTimeout(checkResize, 100)); diff --git a/vscode-wpilib/src/riolog/shared/sharedscript.ts b/vscode-wpilib/src/riolog/shared/sharedscript.ts index 49422a6b..08160fad 100644 --- a/vscode-wpilib/src/riolog/shared/sharedscript.ts +++ b/vscode-wpilib/src/riolog/shared/sharedscript.ts @@ -2,198 +2,253 @@ import { IIPCSendMessage, ReceiveTypes, SendTypes } from './interfaces'; import { IErrorMessage, IPrintMessage, MessageType } from './message'; -import { checkResize, scrollImpl, sendMessage } from '../script/implscript'; +import { AnsiSegment, applyAnsiStyling, parseAnsiString } from '../ansi/ansiparser'; + +// Define these functions here, they'll be implemented in implscript.ts +let checkResize: () => void; +let scrollImpl: () => void; +let sendMessage: (message: any) => void; + +// Function to set the implementation functions from implscript +export function setImplFunctions( + checkResizeImpl: () => void, + scrollImplFunc: () => void, + sendMessageFunc: (message: any) => void +) { + checkResize = checkResizeImpl; + scrollImpl = scrollImplFunc; + sendMessage = sendMessageFunc; + setLivePage(); +} let paused = false; +let discard = false; +let showWarnings = true; +let showPrints = true; +let autoReconnect = true; +let showTimestamps = false; +let autoScroll = true; +let filterText = ''; +const maxLogEntries = 2000; + +let UI_COLORS = { + success: '#4caf50', + warning: '#ff9800', + error: '#f44336', + info: '#2196f3', +}; + +function updateThemeColors(colors: Record) { + UI_COLORS = { ...UI_COLORS, ...colors }; +} + export function onPause() { - const pauseElement = document.getElementById('pause'); + paused = !paused; + const pauseElement = document.getElementById('pause-button'); if (pauseElement === null) { return; } - if (paused === true) { - paused = false; - pauseElement.innerHTML = 'Pause'; - sendMessage({ - message: false, - type: ReceiveTypes.Pause, - }); - } else { - paused = true; + + if (paused) { pauseElement.innerHTML = 'Paused: 0'; - sendMessage({ - message: true, - type: ReceiveTypes.Pause, - }); + pauseElement.classList.add('active'); + } else { + pauseElement.innerHTML = 'Pause'; + pauseElement.classList.remove('active'); } + + sendMessage({ + message: paused, + type: ReceiveTypes.Pause, + }); } -let discard = false; export function onDiscard() { - const dButton = document.getElementById('discard'); + discard = !discard; + const dButton = document.getElementById('discard-button'); if (dButton === null) { return; } - if (discard === true) { - discard = false; - dButton.innerHTML = 'Discard'; - sendMessage({ - message: false, - type: ReceiveTypes.Discard, - }); + + if (discard) { + dButton.innerHTML = 'Resume Capture'; + dButton.classList.add('active'); } else { - discard = true; - dButton.innerHTML = 'Resume'; - sendMessage({ - message: true, - type: ReceiveTypes.Discard, - }); + dButton.innerHTML = 'Discard'; + dButton.classList.remove('active'); } + + sendMessage({ + message: discard, + type: ReceiveTypes.Discard, + }); } export function onClear() { - const list = document.getElementById('list'); + const list = document.getElementById('log-container'); if (list === null) { return; } list.innerHTML = ''; } -let showWarnings = true; +export function onAutoScrollToggle() { + autoScroll = !autoScroll; + const scrollButton = document.getElementById('autoscroll-button'); + if (scrollButton === null) { + return; + } + + if (autoScroll) { + scrollButton.innerHTML = 'Auto-Scroll: On'; + scrollButton.classList.remove('active'); + scrollImpl(); + } else { + scrollButton.innerHTML = 'Auto-Scroll: Off'; + scrollButton.classList.add('active'); + } +} + export function onShowWarnings() { - const warningsButton = document.getElementById('showwarnings'); + showWarnings = !showWarnings; + const warningsButton = document.getElementById('warnings-button'); if (warningsButton === null) { return; } - if (showWarnings === true) { - showWarnings = false; - warningsButton.innerHTML = 'Show Warnings'; + + if (showWarnings) { + warningsButton.innerHTML = 'Hide Warnings'; + warningsButton.classList.remove('active'); } else { - showWarnings = true; - warningsButton.innerHTML = "Don't Show Warnings"; + warningsButton.innerHTML = 'Show Warnings'; + warningsButton.classList.add('active'); } - const ul = document.getElementById('list'); - if (ul === null) { + + const container = document.getElementById('log-container'); + if (container === null) { return; } - const items = ul.getElementsByTagName('li'); + + const items = container.getElementsByClassName('warning-log'); for (let i = 0; i < items.length; ++i) { - if (items[i].dataset.type === 'warning') { - if (showWarnings === true) { - items[i].style.display = 'inline'; - } else { - items[i].style.display = 'none'; - } + if (showWarnings) { + (items[i] as HTMLElement).style.display = 'block'; + } else { + (items[i] as HTMLElement).style.display = 'none'; } } + + applyLogFilter(); checkResize(); } -let showPrints = true; export function onShowPrints() { - const printButton = document.getElementById('showprints'); + showPrints = !showPrints; + const printButton = document.getElementById('prints-button'); if (printButton === null) { return; } - if (showPrints === true) { - showPrints = false; - printButton.innerHTML = 'Show Prints'; + + if (showPrints) { + printButton.innerHTML = 'Hide Prints'; + printButton.classList.remove('active'); } else { - showPrints = true; - printButton.innerHTML = "Don't Show Prints"; + printButton.innerHTML = 'Show Prints'; + printButton.classList.add('active'); } - const ul = document.getElementById('list'); - if (ul === null) { + + const container = document.getElementById('log-container'); + if (container === null) { return; } - const items = ul.getElementsByTagName('li'); + + const items = container.getElementsByClassName('print-log'); for (let i = 0; i < items.length; ++i) { - if (items[i].dataset.type === 'print') { - if (showPrints === true) { - items[i].style.display = 'inline'; - } else { - items[i].style.display = 'none'; - } + if (showPrints) { + (items[i] as HTMLElement).style.display = 'block'; + } else { + (items[i] as HTMLElement).style.display = 'none'; } } + + applyLogFilter(); checkResize(); } -let autoReconnect = true; export function onAutoReconnect() { - if (autoReconnect === true) { - autoReconnect = false; - // send a disconnect - sendMessage({ - message: false, - type: ReceiveTypes.Reconnect, - }); - } else { - autoReconnect = true; - sendMessage({ - message: true, - type: ReceiveTypes.Reconnect, - }); - } - const arButton = document.getElementById('autoreconnect'); + autoReconnect = !autoReconnect; + + // Send message to backend + sendMessage({ + message: autoReconnect, + type: ReceiveTypes.Reconnect, + }); + + const arButton = document.getElementById('reconnect-button'); if (arButton === null) { return; } - if (autoReconnect === true) { - arButton.innerHTML = 'Reconnect'; + + if (autoReconnect) { + arButton.innerHTML = 'Auto-Reconnect: On'; + arButton.classList.remove('active'); } else { - arButton.innerHTML = 'Disconnect'; + arButton.innerHTML = 'Auto-Reconnect: Off'; + arButton.classList.add('active'); + + // Force disconnect when auto-reconnect is disabled + sendMessage({ + message: false, + type: ReceiveTypes.Reconnect, + }); } } -let showTimestamps = false; export function onShowTimestamps() { - const tsButton = document.getElementById('timestamps'); + showTimestamps = !showTimestamps; + const tsButton = document.getElementById('timestamps-button'); if (tsButton === null) { return; } - if (showTimestamps === true) { - showTimestamps = false; - tsButton.innerHTML = 'Show Timestamps'; + + if (showTimestamps) { + tsButton.innerHTML = 'Hide Timestamps'; + tsButton.classList.remove('active'); } else { - showTimestamps = true; - tsButton.innerHTML = "Don't Show Timestamps"; + tsButton.innerHTML = 'Show Timestamps'; + tsButton.classList.add('active'); } - const ul = document.getElementById('list'); - if (ul === null) { + + const container = document.getElementById('log-container'); + if (container === null) { return; } - const items = ul.getElementsByTagName('li'); - for (let i = 0; i < items.length; ++i) { - const spans = items[i].getElementsByTagName('span'); - if (spans === undefined) { - continue; - } - for (let j = 0; j < spans.length; j++) { - const span = spans[j]; - if (span.hasAttribute('data-timestamp')) { - if (showTimestamps === true) { - span.style.display = 'inline'; - } else { - span.style.display = 'none'; - } - } + + const timestamps = container.getElementsByClassName('timestamp'); + for (let i = 0; i < timestamps.length; ++i) { + if (showTimestamps) { + (timestamps[i] as HTMLElement).style.display = 'inline'; + } else { + (timestamps[i] as HTMLElement).style.display = 'none'; } } + checkResize(); } export function onSaveLog() { - const ul = document.getElementById('list'); - if (ul === null) { + const container = document.getElementById('log-container'); + if (container === null) { return; } - const items = ul.getElementsByTagName('li'); + + const items = container.getElementsByClassName('log-entry'); const logs: string[] = []; for (let i = 0; i < items.length; ++i) { - const m = items[i].dataset.message; - if (m === undefined) { - return; + const m = items[i].getAttribute('data-message'); + if (m === null) { + continue; } logs.push(m); } @@ -204,63 +259,200 @@ export function onSaveLog() { }); } -export function onConnect() { - const button = document.getElementById('autoreconnect'); - if (button === null) { +export function onSearch(event: Event) { + const input = event.target as HTMLInputElement; + filterText = input.value.toLowerCase(); + applyLogFilter(); +} + +function applyLogFilter() { + if (filterText === '') { + // Show all entries that should be visible based on their type + const container = document.getElementById('log-container'); + if (container === null) { + return; + } + + const items = container.getElementsByClassName('log-entry'); + for (let i = 0; i < items.length; ++i) { + const item = items[i] as HTMLElement; + const type = item.getAttribute('data-type'); + + if (type === 'warning' && !showWarnings) { + item.style.display = 'none'; + } else if (type === 'print' && !showPrints) { + item.style.display = 'none'; + } else { + item.style.display = 'block'; + } + } + return; + } + + // Filter based on text content + const container = document.getElementById('log-container'); + if (container === null) { return; } - button.style.backgroundColor = 'Green'; + + const items = container.getElementsByClassName('log-entry'); + for (let i = 0; i < items.length; ++i) { + const item = items[i] as HTMLElement; + const type = item.getAttribute('data-type'); + + // Skip if it's already hidden by type filters + if ((type === 'warning' && !showWarnings) || (type === 'print' && !showPrints)) { + item.style.display = 'none'; + continue; + } + + const content = item.textContent?.toLowerCase() || ''; + if (content.includes(filterText)) { + item.style.display = 'block'; + } else { + item.style.display = 'none'; + } + } +} + +export function onConnect() { + const statusIndicator = document.getElementById('connection-status'); + if (statusIndicator) { + statusIndicator.className = 'connection-status connected'; + statusIndicator.setAttribute('title', 'Connected to Robot'); + } + + // Add connection message + const connectedMessage: IPrintMessage = { + line: '\u001b[32mRobot connection established\u001b[0m', + messageType: MessageType.Print, + seqNumber: 0, + timestamp: Date.now() / 1000, + }; + addMessage(connectedMessage); } export function onDisconnect() { - const button = document.getElementById('autoreconnect'); - if (button === null) { + const statusIndicator = document.getElementById('connection-status'); + if (statusIndicator) { + statusIndicator.className = 'connection-status disconnected'; + statusIndicator.setAttribute('title', 'Disconnected from Robot'); + } + + // Add disconnection message + const disconnectedMessage: IPrintMessage = { + line: '\u001b[31mRobot connection lost\u001b[0m', + messageType: MessageType.Print, + seqNumber: 0, + timestamp: Date.now() / 1000, + }; + addMessage(disconnectedMessage); +} + +export function onChangeTeamNumber() { + const input = document.getElementById('team-number') as HTMLInputElement; + if (!input) { + return; + } + + const teamNumber = parseInt(input.value, 10); + if (isNaN(teamNumber) || teamNumber < 0 || teamNumber > 99999) { + // Visual indication of invalid input + input.classList.add('error'); + setTimeout(() => input.classList.remove('error'), 1000); return; } - button.style.backgroundColor = 'Red'; + + sendMessage({ + message: teamNumber, + type: ReceiveTypes.ChangeNumber, + }); + + // Visual confirmation + const button = document.getElementById('team-number-button'); + if (button) { + const originalText = button.textContent || ''; + button.textContent = '✓ Applied'; + button.classList.add('success'); + + setTimeout(() => { + button.textContent = originalText; + button.classList.remove('success'); + }, 1000); + } } -function insertMessage(ts: number, line: string, li: HTMLLIElement, color?: string) { - const div = document.createElement('div'); +// Create a styled segment from ANSI parsed content +function createStyledElement(segment: AnsiSegment): HTMLSpanElement { + const span = document.createElement('span'); + const tNode = document.createTextNode(segment.text); + span.appendChild(tNode); + applyAnsiStyling(span, segment.state); + return span; +} + +function insertMessage(ts: number, line: string, li: HTMLElement, color?: string) { + const messageContent = document.createElement('div'); + messageContent.className = 'log-message'; + + // Create timestamp element const tsSpan = document.createElement('span'); - tsSpan.appendChild(document.createTextNode(ts.toFixed(3) + ': ')); - tsSpan.dataset.timestamp = 'true'; - if (showTimestamps === true) { - tsSpan.style.display = 'inline'; - } else { + tsSpan.className = 'timestamp'; + tsSpan.textContent = new Date(ts * 1000).toISOString().slice(11, -1) + ': '; + if (!showTimestamps) { tsSpan.style.display = 'none'; } - div.appendChild(tsSpan); - const span = document.createElement('span'); - const split = line.split('\n'); + messageContent.appendChild(tsSpan); + + // Create the content container + const contentSpan = document.createElement('span'); + contentSpan.className = 'message-content'; + if (color !== undefined) { + contentSpan.style.color = color; + } + + // Process each line + const lines = line.split('\n'); let first = true; - for (const item of split) { + + for (const item of lines) { if (item.trim() === '') { continue; } - if (first === false) { - span.appendChild(document.createElement('br')); + + if (!first) { + contentSpan.appendChild(document.createElement('br')); } first = false; - const tNode = document.createTextNode(item); - span.appendChild(tNode); - } - if (color !== undefined) { - span.style.color = color; + + // Check if the line contains ANSI codes + if (item.includes('\u001b[')) { + const segments = parseAnsiString(item); + for (const segment of segments) { + contentSpan.appendChild(createStyledElement(segment)); + } + } else { + // No ANSI codes, just append text + const tNode = document.createTextNode(item); + contentSpan.appendChild(tNode); + } } - div.appendChild(span); - li.appendChild(div); + + messageContent.appendChild(contentSpan); + li.appendChild(messageContent); } -function insertStackTrace(st: string, li: HTMLLIElement, color?: string) { +function insertStackTrace(st: string, container: HTMLElement, color?: string) { const div = document.createElement('div'); + div.className = 'stack-trace'; + const split = st.split('\n'); let first = true; for (const item of split) { if (item.trim() === '') { continue; } - if (first === false) { + if (!first) { div.appendChild(document.createElement('br')); } first = false; @@ -270,28 +462,30 @@ function insertStackTrace(st: string, li: HTMLLIElement, color?: string) { if (color !== undefined) { div.style.color = color; } - li.appendChild(div); + container.appendChild(div); } -function insertLocation(loc: string, li: HTMLLIElement, color?: string) { +function insertLocation(loc: string, container: HTMLElement, color?: string) { const div = document.createElement('div'); + div.className = 'location-info'; + const split = loc.split('\n'); let first = true; for (const item of split) { if (item.trim() === '') { continue; } - if (first === false) { - li.appendChild(document.createElement('br')); + if (!first) { + div.appendChild(document.createElement('br')); } first = false; const tNode = document.createTextNode('\u00a0\u00a0 from: ' + item); - li.appendChild(tNode); + div.appendChild(tNode); } if (color !== undefined) { div.style.color = color; } - li.appendChild(div); + container.appendChild(div); } export function addMessage(message: IPrintMessage | IErrorMessage) { @@ -303,308 +497,489 @@ export function addMessage(message: IPrintMessage | IErrorMessage) { } function limitList() { - const ul = document.getElementById('list') as HTMLUListElement; - if (ul === null) { + const container = document.getElementById('log-container'); + if (container === null) { return; } - if (ul.childElementCount > 1000 && ul.firstChild !== null) { - ul.removeChild(ul.firstChild); + + while (container.childElementCount > maxLogEntries && container.firstChild !== null) { + container.removeChild(container.firstChild); } } export function addPrint(message: IPrintMessage) { limitList(); - const ul = document.getElementById('list') as HTMLUListElement; - if (ul === null) { + const container = document.getElementById('log-container'); + if (container === null) { return; } - const li = document.createElement('li'); - li.style.fontFamily = 'Consolas, "Courier New", monospace'; - insertMessage(message.timestamp, message.line, li); - const str = JSON.stringify(message); - li.dataset.message = str; - li.dataset.type = 'print'; - if (showPrints === true) { - li.style.display = 'inline'; - } else { - li.style.display = 'none'; + + const entry = document.createElement('div'); + entry.className = 'log-entry print-log'; + entry.setAttribute('data-type', 'print'); + entry.setAttribute('data-message', JSON.stringify(message)); + + if (!showPrints) { + entry.style.display = 'none'; + } + + insertMessage(message.timestamp, message.line, entry); + container.appendChild(entry); + + // Apply search filter if needed + if (filterText !== '') { + const content = entry.textContent?.toLowerCase() || ''; + if (!content.includes(filterText)) { + entry.style.display = 'none'; + } + } + + if (autoScroll) { + scrollImpl(); } - ul.appendChild(li); } -export function expandError(message: IErrorMessage, li: HTMLLIElement, color?: string) { +// Creates HTML for an expanded error view +export function createErrorContent(message: IErrorMessage, container: HTMLElement, color?: string) { + // Clear existing content first + container.innerHTML = ''; + // First append the message - insertMessage(message.timestamp, message.details, li, color); + insertMessage(message.timestamp, message.details, container, color); + // Then append location, tabbed in once - insertLocation(message.location, li); + insertLocation(message.location, container, color); + // Then append stack trace, tabbed in twice - insertStackTrace(message.callStack, li); - li.appendChild(document.createElement('br')); + insertStackTrace(message.callStack, container, color); +} + +// Create HTML for a collapsed error view +export function createCollapsedErrorContent( + message: IErrorMessage, + container: HTMLElement, + color?: string +) { + // Clear existing content + container.innerHTML = ''; + + // Just show the error message + insertMessage(message.timestamp, message.details, container, color); } export function addError(message: IErrorMessage) { limitList(); - const ul = document.getElementById('list'); - if (ul === null) { + const container = document.getElementById('log-container'); + if (container === null) { return; } - const li = document.createElement('li'); - li.style.fontFamily = 'Consolas, "Courier New", monospace'; - const str = JSON.stringify(message); - li.dataset.expanded = 'false'; - li.dataset.message = str; - if (message.messageType === MessageType.Warning) { - li.dataset.type = 'warning'; - insertMessage(message.timestamp, message.details, li, 'Yellow'); - if (showWarnings === true) { - li.style.display = 'inline'; - } else { - li.style.display = 'none'; - } - } else { - li.dataset.type = 'error'; - insertMessage(message.timestamp, message.details, li, 'Red'); - } - li.onclick = () => { - if (li.dataset.expanded === 'true') { - // shrink - li.dataset.expanded = 'false'; - if (li.dataset.message === undefined) { - return; + + const entry = document.createElement('div'); + entry.className = + message.messageType === MessageType.Warning ? 'log-entry warning-log' : 'log-entry error-log'; + entry.setAttribute('data-expanded', 'false'); + entry.setAttribute( + 'data-type', + message.messageType === MessageType.Warning ? 'warning' : 'error' + ); + entry.setAttribute('data-message', JSON.stringify(message)); + + // Hide warnings if they're filtered out + if (message.messageType === MessageType.Warning && !showWarnings) { + entry.style.display = 'none'; + } + + // Create the toggle button + const toggleButton = document.createElement('div'); + toggleButton.className = 'toggle-button collapsed'; + entry.appendChild(toggleButton); + + // Create the content container + const contentContainer = document.createElement('div'); + contentContainer.className = 'error-content collapsed'; + entry.appendChild(contentContainer); + + // Add the initial collapsed view content + const textColor = + message.messageType === MessageType.Warning + ? 'var(--vscode-warningForeground, ' + UI_COLORS.warning + ')' + : 'var(--vscode-testing-iconFailed, ' + UI_COLORS.error + ')'; + + createCollapsedErrorContent(message, contentContainer, textColor); + + // Add click handler to toggle expansion + entry.addEventListener('click', function () { + const isExpanded = this.getAttribute('data-expanded') === 'true'; + const toggleBtn = this.querySelector('.toggle-button'); + const contentCtr = this.querySelector('.error-content'); + + if (isExpanded) { + // Collapse + this.setAttribute('data-expanded', 'false'); + if (toggleBtn) { + toggleBtn.className = 'toggle-button collapsed'; } - const parsed = JSON.parse(li.dataset.message) as IPrintMessage | IErrorMessage; - li.innerHTML = ''; - if (li.dataset.type === 'warning') { - insertMessage(parsed.timestamp, (parsed as IErrorMessage).details, li, 'Yellow'); - } else { - insertMessage(parsed.timestamp, (parsed as IErrorMessage).details, li, 'Red'); + if (contentCtr) { + contentCtr.className = 'error-content collapsed'; + createCollapsedErrorContent(message, contentCtr as HTMLElement, textColor); } } else { - // expand - li.dataset.expanded = 'true'; - if (li.dataset.message === undefined) { - return; + // Expand + this.setAttribute('data-expanded', 'true'); + if (toggleBtn) { + toggleBtn.className = 'toggle-button expanded'; } - const parsed = JSON.parse(li.dataset.message); - li.innerHTML = ''; - if (li.dataset.type === 'warning') { - expandError(parsed as IErrorMessage, li, 'Yellow'); - } else { - expandError(parsed as IErrorMessage, li, 'Red'); + if (contentCtr) { + contentCtr.className = 'error-content expanded'; + createErrorContent(message, contentCtr as HTMLElement, textColor); } } + checkResize(); - }; - ul.appendChild(li); -} + }); -window.addEventListener('resize', () => { - checkResize(); -}); + container.appendChild(entry); -function handleFileSelect(evt: Event) { - const files = (evt.target as HTMLInputElement).files!; - const firstFile = files[0]; - const reader = new FileReader(); - reader.onload = (loaded: Event) => { - const target: FileReader = loaded.target as FileReader; - const parsed = JSON.parse(target.result as string) as (IPrintMessage | IErrorMessage)[]; - for (const p of parsed) { - addMessage(p); + // Apply search filter if needed + if (filterText !== '') { + const content = entry.textContent?.toLowerCase() || ''; + if (!content.includes(filterText)) { + entry.style.display = 'none'; } - checkResize(); - }; - reader.readAsText(firstFile); -} + } -let currentScreenHeight = 100; + if (autoScroll) { + scrollImpl(); + } +} -export function checkResizeImpl(element: HTMLElement) { - const allowedHeight = element.clientHeight - currentScreenHeight; - const ul = document.getElementById('list'); - if (ul === null) { +export function checkResizeImpl() { + const container = document.getElementById('log-container'); + if (container === null) { return; } - const listHeight = ul.clientHeight; - if (listHeight < allowedHeight) { - ul.style.position = 'fixed'; - ul.style.bottom = currentScreenHeight + 'px'; - } else { - ul.style.position = 'static'; - ul.style.bottom = 'auto'; - } + // Container height is managed through CSS } -export function handleMessage(data: IIPCSendMessage): void { +export function handleMessage(data: IIPCSendMessage | any): void { + console.log(data); + // Handle theme color update message + if (data.type === 'themeColors') { + updateThemeColors(data.message); + return; + } + switch (data.type) { case SendTypes.New: addMessage(data.message as IPrintMessage | IErrorMessage); - scrollImpl(); break; + case SendTypes.Batch: for (const message of data.message as (IPrintMessage | IErrorMessage)[]) { addMessage(message); } - scrollImpl(); break; + case SendTypes.PauseUpdate: - const pause = document.getElementById('pause'); - if (pause !== null) { - pause.innerHTML = 'Paused: ' + (data.message as number); + const pauseButton = document.getElementById('pause-button'); + if (pauseButton !== null) { + pauseButton.innerHTML = `Paused: ${data.message}`; } break; + case SendTypes.ConnectionChanged: - const bMessage: boolean = data.message as boolean; - if (bMessage === true) { + const connected: boolean = data.message as boolean; + if (connected) { onConnect(); } else { onDisconnect(); } break; + default: break; } - checkResize(); } -function createSplitUl(left: boolean): HTMLUListElement { - const splitDiv = document.createElement('ul'); - splitDiv.style.position = 'fixed'; - splitDiv.style.bottom = '0px'; - if (left) { - splitDiv.style.left = '0px'; - } else { - splitDiv.style.right = '0px'; - } - splitDiv.style.listStyleType = 'none'; - splitDiv.style.padding = '0'; - splitDiv.style.width = '49.8%'; - splitDiv.style.marginBottom = '1px'; - return splitDiv; -} - -function createButton(id: string, text: string, callback: () => void): HTMLLIElement { - const li = document.createElement('li'); - const button = document.createElement('button'); - button.id = id; - button.style.width = '100%'; - button.appendChild(document.createTextNode(text)); - button.addEventListener('click', callback); - li.appendChild(button); - return li; -} - -function onChangeTeamNumber() { - const newNumber = document.getElementById('teamNumber'); - console.log('finding team number'); - if (newNumber === null) { - return; - } - console.log('sending message'); - sendMessage({ - message: parseInt((newNumber as HTMLInputElement).value, 10), - type: ReceiveTypes.ChangeNumber, +export function createToolbar(): HTMLElement { + const toolbar = document.createElement('div'); + toolbar.id = 'toolbar'; + toolbar.className = 'toolbar'; + + // Create the connection status indicator + const statusContainer = document.createElement('div'); + statusContainer.className = 'status-container'; + + const connectionStatus = document.createElement('div'); + connectionStatus.id = 'connection-status'; + connectionStatus.className = 'connection-status disconnected'; + connectionStatus.setAttribute('title', 'Disconnected from Robot'); + statusContainer.appendChild(connectionStatus); + + // Create the team number input group + const teamNumberContainer = document.createElement('div'); + teamNumberContainer.className = 'team-number-container'; + + const teamNumberLabel = document.createElement('label'); + teamNumberLabel.htmlFor = 'team-number'; + teamNumberLabel.textContent = 'Team:'; + teamNumberContainer.appendChild(teamNumberLabel); + + const teamNumberInput = document.createElement('input'); + teamNumberInput.type = 'number'; + teamNumberInput.id = 'team-number'; + teamNumberInput.className = 'team-number-input'; + teamNumberInput.min = '1'; + teamNumberInput.max = '99999'; + teamNumberInput.placeholder = 'Team #'; + teamNumberContainer.appendChild(teamNumberInput); + + const teamNumberButton = document.createElement('button'); + teamNumberButton.id = 'team-number-button'; + teamNumberButton.className = 'toolbar-button'; + teamNumberButton.textContent = 'Set'; + teamNumberButton.addEventListener('click', onChangeTeamNumber); + teamNumberContainer.appendChild(teamNumberButton); + + // Search filter + const searchContainer = document.createElement('div'); + searchContainer.className = 'search-container'; + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.id = 'search-input'; + searchInput.className = 'search-input'; + searchInput.placeholder = 'Search logs...'; + searchInput.addEventListener('input', onSearch); + searchContainer.appendChild(searchInput); + + // Button groups + const buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'buttons-container'; + + // Control buttons + const controlButtons = document.createElement('div'); + controlButtons.className = 'button-group'; + + const buttons = [ + { id: 'pause-button', text: 'Pause', handler: onPause, tooltip: 'Pause log updates' }, + { + id: 'discard-button', + text: 'Discard', + handler: onDiscard, + tooltip: 'Discard incoming messages', + }, + { id: 'clear-button', text: 'Clear', handler: onClear, tooltip: 'Clear all log entries' }, + { + id: 'autoscroll-button', + text: 'Auto-Scroll: On', + handler: onAutoScrollToggle, + tooltip: 'Toggle automatic scrolling', + }, + ]; + + buttons.forEach((btn) => { + const button = document.createElement('button'); + button.id = btn.id; + button.className = 'toolbar-button'; + button.textContent = btn.text; + button.title = btn.tooltip; + button.addEventListener('click', btn.handler); + controlButtons.appendChild(button); + }); + + // Filter buttons + const filterButtons = document.createElement('div'); + filterButtons.className = 'button-group'; + + const filterBtns = [ + { + id: 'prints-button', + text: 'Hide Prints', + handler: onShowPrints, + tooltip: 'Toggle print messages', + }, + { + id: 'warnings-button', + text: 'Hide Warnings', + handler: onShowWarnings, + tooltip: 'Toggle warning messages', + }, + { + id: 'timestamps-button', + text: 'Show Timestamps', + handler: onShowTimestamps, + tooltip: 'Toggle timestamps', + active: true, + }, + { + id: 'reconnect-button', + text: 'Auto-Reconnect: On', + handler: onAutoReconnect, + tooltip: 'Toggle auto reconnection', + }, + { id: 'save-button', text: 'Save Log', handler: onSaveLog, tooltip: 'Save log to file' }, + ]; + + filterBtns.forEach((btn) => { + const button = document.createElement('button'); + button.id = btn.id; + button.className = 'toolbar-button'; + if (btn.active) { + button.classList.add('active'); + } + button.textContent = btn.text; + button.title = btn.tooltip; + button.addEventListener('click', btn.handler); + filterButtons.appendChild(button); }); - console.log('sent message'); + + // Add all components to the toolbar + buttonsContainer.appendChild(controlButtons); + buttonsContainer.appendChild(filterButtons); + + toolbar.appendChild(statusContainer); + toolbar.appendChild(teamNumberContainer); + toolbar.appendChild(searchContainer); + toolbar.appendChild(buttonsContainer); + + return toolbar; } -function setLivePage() { - const mdv = document.getElementById('mainDiv'); - if (mdv === undefined) { +export function setLivePage() { + const mainDiv = document.getElementById('mainDiv'); + if (!mainDiv) { return; } - const mainDiv: HTMLDivElement = mdv as HTMLDivElement; - currentScreenHeight = 100; + + // Clear the container mainDiv.innerHTML = ''; - const ul = document.createElement('ul'); - ul.id = 'list'; - ul.style.listStyleType = 'none'; - ul.style.padding = '0'; - mainDiv.appendChild(ul); - const splitDiv = document.createElement('div'); - splitDiv.style.height = '100px'; - mainDiv.appendChild(splitDiv); - const leftList = createSplitUl(true); - leftList.appendChild(createButton('pause', 'Pause', onPause)); - leftList.appendChild(createButton('discard', 'Discard', onDiscard)); - leftList.appendChild(createButton('clear', 'Clear', onClear)); - leftList.appendChild(createButton('showprints', "Don't Show Prints", onShowPrints)); - leftList.appendChild( - createButton('switchPage', 'Switch to Viewer', () => { - setViewerPage(); - }) - ); - mainDiv.appendChild(leftList); - - const rightList = createSplitUl(false); - rightList.appendChild(createButton('showwarnings', "Don't Show Warnings", onShowWarnings)); - rightList.appendChild(createButton('autoreconnect', 'Disconnect', onAutoReconnect)); - rightList.appendChild(createButton('timestamps', 'Show Timestamps', onShowTimestamps)); - rightList.appendChild(createButton('savelot', 'Save Log', onSaveLog)); - const teamNumberUl = document.createElement('li'); - const teamNumberI = document.createElement('input'); - teamNumberI.id = 'teamNumber'; - teamNumberI.type = 'number'; - teamNumberI.style.width = '50%'; - const teamNumberB = document.createElement('button'); - teamNumberB.id = 'changeTeamNumber'; - teamNumberB.style.width = '24.9%'; - teamNumberB.style.right = '0'; - teamNumberB.style.position = 'fixed'; - teamNumberB.addEventListener('click', onChangeTeamNumber); - teamNumberB.appendChild(document.createTextNode('Set Team Number')); - teamNumberUl.appendChild(teamNumberI); - teamNumberUl.appendChild(teamNumberB); - rightList.appendChild(teamNumberUl); - mainDiv.appendChild(rightList); - if (autoReconnect !== true) { - onAutoReconnect(); + + const toolbar = createToolbar(); + toolbar.id = 'toolbar'; + mainDiv.appendChild(toolbar); + + // Create log container + const logContainer = document.createElement('div'); + logContainer.id = 'log-container'; + mainDiv.appendChild(logContainer); + + if (!window.__riologHasLoaded) { + const welcomeMessage: IPrintMessage = { + messageType: MessageType.Print, + line: + '\u001b[1m\u001b[36m=== WPILib RioLog Started ===\u001b[0m\n' + + '\u001b[32mWaiting for robot connection...\u001b[0m\n' + + '\u001b[33mTIPS:\u001b[0m\n' + + '• \u001b[0mUse \u001b[1mSet\u001b[0m button to change team number\n' + + '• \u001b[0mClick on errors/warnings to expand details\n' + + '• \u001b[0mUse search box to filter messages\n' + + '• \u001b[0mToggle auto-scrolling for viewing older logs\n' + + '• \u001b[0mSave logs to file for later analysis', + timestamp: Date.now() / 1000, + seqNumber: 0, + }; + addMessage(welcomeMessage); + window.__riologHasLoaded = true; + } + + // Ensure timestamps button starts in correct state + const timestampsButton = document.getElementById('timestamps-button'); + if (timestampsButton) { + if (!showTimestamps) { + timestampsButton.classList.add('active'); + } else { + timestampsButton.classList.remove('active'); + } + } + + // Ensure warnings button starts in correct state + const warningsButton = document.getElementById('warnings-button'); + if (warningsButton) { + if (!showWarnings) { + warningsButton.classList.add('active'); + warningsButton.textContent = 'Show Warnings'; + } else { + warningsButton.classList.remove('active'); + warningsButton.textContent = 'Hide Warnings'; + } } } export function setViewerPage() { - const mdv = document.getElementById('mainDiv'); - if (mdv === undefined) { - return; - } - if (autoReconnect === true) { - onAutoReconnect(); + setLivePage(); // Reuse the same UI for now with minor modifications + + // Add file input control for loading logs + const toolbar = document.getElementById('toolbar'); + if (toolbar) { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = 'log-file-input'; + fileInput.accept = '.json'; + fileInput.className = 'file-input'; + fileInput.addEventListener('change', handleFileSelect, false); + + const fileLabel = document.createElement('label'); + fileLabel.htmlFor = 'log-file-input'; + fileLabel.textContent = 'Load Log File'; + fileLabel.className = 'toolbar-button'; + + toolbar.insertBefore(fileLabel, toolbar.firstChild); + toolbar.insertBefore(fileInput, toolbar.firstChild); + + // Hide reconnect button in viewer mode + const reconnectButton = document.getElementById('reconnect-button'); + if (reconnectButton) { + reconnectButton.style.display = 'none'; + } + + // Hide team number input in viewer mode + const teamNumberContainer = document.querySelector('.team-number-container') as HTMLElement; + if (teamNumberContainer) { + teamNumberContainer.style.display = 'none'; + } } - const mainDiv: HTMLDivElement = mdv as HTMLDivElement; - currentScreenHeight = 60; - mainDiv.innerHTML = ''; - const ul = document.createElement('ul'); - ul.id = 'list'; - ul.style.listStyleType = 'none'; - ul.style.padding = '0'; - mainDiv.appendChild(ul); - const splitDiv = document.createElement('div'); - splitDiv.style.height = '60px'; - mainDiv.appendChild(splitDiv); - - const leftList = createSplitUl(true); - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.id = 'openFile'; - fileInput.name = 'files[]'; - fileInput.style.width = '100%'; - fileInput.addEventListener('change', handleFileSelect, false); - leftList.appendChild(fileInput); - leftList.appendChild(createButton('showprints', "Don't Show Prints", onShowPrints)); - leftList.appendChild( - createButton('switchPage', 'Switch to Live', () => { - setLivePage(); - }) - ); - mainDiv.appendChild(leftList); +} + +function handleFileSelect(evt: Event) { + const files = (evt.target as HTMLInputElement).files!; + const firstFile = files[0]; + const reader = new FileReader(); + reader.onload = (loaded: Event) => { + const target: FileReader = loaded.target as FileReader; + try { + const parsed = JSON.parse(target.result as string) as (IPrintMessage | IErrorMessage)[]; - const rightList = createSplitUl(false); - rightList.appendChild(createButton('showwarnings', "Don't Show Warnings", onShowWarnings)); - rightList.appendChild(createButton('timestamps', 'Show Timestamps', onShowTimestamps)); + // Clear existing logs first + onClear(); - mainDiv.appendChild(rightList); + // Add all logs from the file + for (const p of parsed) { + addMessage(p); + } + } catch (error) { + console.error('Error parsing log file:', error); + + // Show error message in the log + const errorMessage: IErrorMessage = { + callStack: '', + details: 'Failed to parse log file: ' + (error as Error).message, + errorCode: 0, + flags: 1, + location: '', + messageType: MessageType.Error, + numOccur: 1, + seqNumber: 0, + timestamp: Date.now() / 1000, + }; + addMessage(errorMessage); + } + }; + reader.readAsText(firstFile); } -window.addEventListener('load', (_: Event) => { - setLivePage(); -}); +declare global { + interface Window { + __riologHasLoaded?: boolean; + } +} diff --git a/vscode-wpilib/src/riolog/vscodeimpl.ts b/vscode-wpilib/src/riolog/vscodeimpl.ts index 5d0ba8e9..bfca9bed 100644 --- a/vscode-wpilib/src/riolog/vscodeimpl.ts +++ b/vscode-wpilib/src/riolog/vscodeimpl.ts @@ -3,7 +3,6 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as vscode from 'vscode'; -import { readFileAsync } from '../utilities'; import { IIPCReceiveMessage, IIPCSendMessage, @@ -14,6 +13,7 @@ import { } from './shared/interfaces'; import { RioConsole } from './rioconsole'; import { IErrorMessage, IPrintMessage } from './shared/message'; +import { readFileAsync } from '../utilities'; interface IHTMLProvider { getHTML(webview: vscode.Webview): string; @@ -35,7 +35,7 @@ export class RioLogWindowView extends EventEmitter implements IWindowView { this.webview.onDidChangeViewState( (s) => { - if (s.webviewPanel.visible === true) { + if (s.webviewPanel.visible) { this.emit('windowActive'); } }, @@ -44,20 +44,34 @@ export class RioLogWindowView extends EventEmitter implements IWindowView { ); this.webview.webview.onDidReceiveMessage( - (data: IIPCReceiveMessage) => { - this.emit('didReceiveMessage', data); - }, + (data: IIPCReceiveMessage) => this.emit('didReceiveMessage', data), null, this.disposables ); - this.webview.onDidDispose( - () => { - this.emit('didDispose'); - }, - null, - this.disposables - ); + this.webview.onDidDispose(() => this.emit('didDispose'), null, this.disposables); + + // Send theme colors when created + this.sendThemeColors(); + + // Listen for theme changes and update colors + vscode.window.onDidChangeActiveColorTheme(() => this.sendThemeColors(), null, this.disposables); + } + + private sendThemeColors() { + // Extract key colors from the current theme + const colors = { + // These don't have direct VSCode equivalents, so we use custom colors + success: '#4caf50', + warning: '#ff9800', + error: '#f44336', + info: '#2196f3', + }; + + this.webview.webview.postMessage({ + type: 'themeColors', + message: colors, + }); } public getWebview(): vscode.Webview { @@ -67,9 +81,11 @@ export class RioLogWindowView extends EventEmitter implements IWindowView { public setHTML(html: string): void { this.webview.webview.html = html; } + public async postMessage(message: IIPCSendMessage): Promise { return this.webview.webview.postMessage(message); } + public dispose() { for (const d of this.disposables) { d.dispose(); @@ -77,12 +93,25 @@ export class RioLogWindowView extends EventEmitter implements IWindowView { } public async handleSave(saveData: (IPrintMessage | IErrorMessage)[]): Promise { - const d = await vscode.workspace.openTextDocument({ - content: JSON.stringify(saveData, null, 4), - language: 'json', + const uri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file('riolog.json'), + filters: { 'JSON Files': ['json'] }, + saveLabel: 'Save RioLog', }); - await vscode.window.showTextDocument(d); - return true; + + if (!uri) { + return false; + } + + try { + await vscode.workspace.fs.writeFile(uri, Buffer.from(JSON.stringify(saveData, null, 2))); + + vscode.window.showInformationMessage(`RioLog saved to ${uri.fsPath}`); + return true; + } catch (err) { + vscode.window.showErrorMessage(`Failed to save RioLog: ${err}`); + return false; + } } } @@ -102,16 +131,32 @@ export class RioLogHTMLProvider implements IHTMLProvider { } public getHTML(webview: vscode.Webview): string { - const onDiskPath = vscode.Uri.file(path.join(this.resourceRoot, 'dist', 'riologpage.js')); + // Get paths to script and CSS + const scriptPath = vscode.Uri.file( + path.join(this.resourceRoot, '..', 'resources', 'dist', 'riologpage.js') + ); + const cssPath = vscode.Uri.file(path.join(this.resourceRoot, '..', 'media', 'main.css')); - // And get the special URI to use with the webview - const scriptResourcePath = webview.asWebviewUri(onDiskPath); + // Convert to webview URIs + const scriptUri = webview.asWebviewUri(scriptPath); + const cssUri = webview.asWebviewUri(cssPath); let html = this.html!; - html += '\r\n\r\n'; + + // Add CSS link + html = html.replace('', `\r\n`); + + // Add script with error handling + html = html.replace( + '', + ` + + ` + ); return html; } diff --git a/vscode-wpilib/webpack.config.js b/vscode-wpilib/webpack.config.js index aba36aa4..8144481b 100644 --- a/vscode-wpilib/webpack.config.js +++ b/vscode-wpilib/webpack.config.js @@ -6,10 +6,9 @@ module.exports = [ mode: 'none', entry: { localeloader: './src/webviews/localeloader.ts', - gradle2025importpage: './src/webviews/pages/gradle2025importpage.ts', projectcreatorpage: './src/webviews/pages/projectcreatorpage.ts', - riologpage: './src/riolog/shared/sharedscript.ts', + riologpage: './src/riolog/script/implscript.ts', }, devtool: 'inline-source-map', module: { @@ -27,9 +26,6 @@ module.exports = [ resolve: { extensions: ['.ts', '.js'], }, - node: { - net: 'empty', - }, output: { path: path.resolve(__dirname, 'resources', 'dist'), filename: '[name].js',