Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/frontend/components/_atoms/tab/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GitCompare } from 'lucide-react'
import type React from 'react'
import { ComponentPropsWithoutRef, useCallback } from 'react'

Expand Down Expand Up @@ -55,6 +56,7 @@
'ethercat-device': <DeviceTransferIcon className='h-4 w-4 flex-shrink-0' />,
'library-manager': <LibraryIcon className='h-4 w-4 flex-shrink-0' />,
'library-manifest': <LibraryManifestIcon className='h-4 w-4 flex-shrink-0' />,
'diff-viewer': <GitCompare className='h-4 w-4 flex-shrink-0 text-[#0464FB]' />,
}

const Tab = (props: ITabProps) => {
Expand Down Expand Up @@ -84,7 +86,8 @@
| 'package-manager'
| 'ethercat-device'
| 'library-manager'
| 'library-manifest' = 'il'
| 'library-manifest'
| 'diff-viewer' = 'il'

if (fileDerivation?.type === 'data-type' || fileDerivation?.type === 'device') {
languageOrDerivation = fileDerivation?.derivation
Expand Down Expand Up @@ -120,6 +123,9 @@
if (fileDerivation?.type === 'library-manifest') {
languageOrDerivation = 'library-manifest'
}
if (fileDerivation?.type === 'diff-viewer') {
languageOrDerivation = 'diff-viewer'
}

const { file: associatedFile } = getFile({ name: fileName || '' })
const handleFileName = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* FileDiffView — single entry point for rendering a before/after diff of one
* project file. Routes graphical POUs (.ld/.fbd) to the GraphicalDiffViewer
* and everything else to Monaco's DiffEditor. Shared by the history page and
* the source-control diff tab so both surfaces render diffs identically.
*/

import { DiffEditor } from '@monaco-editor/react'

import { GraphicalDiffViewer, isGraphicalFile } from './graphical-diff-viewer'

export { isGraphicalFile }

/** Map a file path to the Monaco language id used for syntax highlighting. */
export function getLanguageFromPath(path: string): string {
const ext = path.split('.').pop()?.toLowerCase()
switch (ext) {
case 'json':
return 'json'
case 'st':
case 'il':
case 'sfc':
return 'st'
case 'py':
return 'python'
case 'c':
return 'c'
case 'cpp':
return 'cpp'
default:
return 'plaintext'
}
}

/**
* For graphical files (.ld/.fbd) shown in the textual diff, the embedded JSON
* flow blob is noise — collapse it to a placeholder so the textual diff stays
* focused on the variable declarations. Non-graphical files pass through
* untouched. (Graphical files normally route to GraphicalDiffViewer, but this
* keeps the helper safe if it is ever used on the textual side.)
*/
export function formatContentForDisplay(path: string, content: string): string {
const ext = path.split('.').pop()?.toLowerCase()
if (ext !== 'ld' && ext !== 'fbd') return content

const endMatch = content.match(/\b(END_PROGRAM|END_FUNCTION_BLOCK|END_FUNCTION)\b/i)
if (!endMatch || endMatch.index === undefined) return content

const endKeyword = endMatch[0]
const beforeEnd = content.slice(0, endMatch.index)

const endVarIdx = beforeEnd.lastIndexOf('END_VAR')
if (endVarIdx === -1) return content

const declaration = beforeEnd.slice(0, endVarIdx + 'END_VAR'.length)
return `${declaration}\n\n(* ${ext.toUpperCase()} graphical data omitted *)\n\n${endKeyword}`
}
Comment on lines +42 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Case-sensitivity inconsistency in END_VAR search.

The regex on line 46 uses the /i flag for case-insensitive matching of END_PROGRAM/END_FUNCTION_BLOCK/END_FUNCTION, but line 52 uses lastIndexOf('END_VAR') which is case-sensitive. IEC 61131-3 ST language is case-insensitive, so files with end_var or End_Var would fail to collapse the graphical blob.

🔧 Proposed fix for case-insensitive matching
-  const endVarIdx = beforeEnd.lastIndexOf('END_VAR')
-  if (endVarIdx === -1) return content
+  const endVarMatch = beforeEnd.match(/END_VAR/i)
+  if (!endVarMatch || endVarMatch.index === undefined) return content
+  const endVarIdx = endVarMatch.index

-  const declaration = beforeEnd.slice(0, endVarIdx + 'END_VAR'.length)
+  const declaration = beforeEnd.slice(0, endVarIdx + endVarMatch[0].length)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/diff-viewer/file-diff-view.tsx
around lines 42 - 57, In formatContentForDisplay, the search for "END_VAR" is
case-sensitive (endVarIdx = beforeEnd.lastIndexOf('END_VAR')) while the end
keyword match is case-insensitive; make the END_VAR lookup case-insensitive to
match IEC 61131-3 semantics by searching beforeEnd in a case-insensitive way
(e.g., use a case-insensitive regex search or normalize case before calling
lastIndexOf) so endVarIdx correctly finds variants like "end_var" or "End_Var",
then build declaration and return as before.


type FileDiffViewProps = {
filePath: string
/** Original (e.g. HEAD / previous-commit) content. Empty string for added files. */
original: string
/** Current (e.g. working-tree / commit) content. Empty string for deleted files. */
current: string
isDark: boolean
}

export function FileDiffView({ filePath, original, current, isDark }: FileDiffViewProps) {
if (isGraphicalFile(filePath)) {
return (
<GraphicalDiffViewer originalContent={original} currentContent={current} filePath={filePath} isDark={isDark} />
)
}

return (
<DiffEditor
original={formatContentForDisplay(filePath, original)}
modified={formatContentForDisplay(filePath, current)}
language={getLanguageFromPath(filePath)}
theme={isDark ? 'vs-dark' : 'vs'}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 12,
scrollBeyondLastLine: false,
domReadOnly: true,
renderSideBySide: true,
originalEditable: false,
}}
/>
)
}
Loading
Loading