full PC config setup

shifting towards manual symlinks for more complex setup than laptop, allows for GUI settings changes to actually apply, and is much easier than nix-ifying every config
This commit is contained in:
greyweather committed 2026-07-12 04:01:28 +01:00
1 parent 19e8f0a6d5
commit 0f018cd2e3
149 files changed
+9539 -478

No files matched your search

@@ -0,0 +1,561 @@
body {
padding: 0;
margin: 0;
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
}
.container {
display: flex;
height: 100vh;
gap: 0;
background: var(--vscode-panel-border);
position: relative;
}
/* Left panel - Pattern list */
.pattern-list-panel {
flex: 0 0 300px;
min-width: 200px;
max-width: 600px;
background: var(--vscode-sideBar-background);
display: flex;
flex-direction: column;
}
/* Resize Handle */
.resize-handle {
flex: 0 0 4px;
background: var(--vscode-panel-border);
cursor: ew-resize;
position: relative;
user-select: none;
}
.resize-handle:hover {
background: var(--vscode-focusBorder);
}
.resize-handle.resizing {
background: var(--vscode-focusBorder);
}
.search-box {
padding: 8px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-panel-border);
position: relative;
}
.search-wrapper {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 8px;
pointer-events: none;
color: var(--vscode-input-placeholderForeground);
font-size: 14px;
}
.search-input {
width: 100%;
padding: 6px 30px 6px 30px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
font-size: 13px;
box-sizing: border-box;
}
.search-input:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.search-clear {
position: absolute;
right: 6px;
padding: 4px;
background: transparent;
border: none;
cursor: pointer;
color: var(--vscode-input-placeholderForeground);
border-radius: 3px;
display: none;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
transition: all 0.15s ease;
}
.search-clear:hover {
background: var(--vscode-toolbar-hoverBackground);
color: var(--vscode-foreground);
}
.search-clear.visible {
display: flex;
}
.search-clear .codicon {
font-size: 12px;
}
.pattern-list {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.section-header {
padding: 8px 12px;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
color: var(--vscode-foreground);
opacity: 0.6;
letter-spacing: 0.5px;
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.section-header-left {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
flex: 1;
}
.add-pattern-btn {
padding: 4px;
background: transparent;
border: none;
cursor: pointer;
color: var(--vscode-foreground);
opacity: 0.6;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
transition: all 0.15s ease;
}
.add-pattern-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
.add-pattern-btn .codicon {
font-size: 14px;
}
.section-header:hover {
opacity: 0.8;
}
.section-header:first-child {
margin-top: 0;
}
.section-header .codicon {
font-size: 14px;
transition: transform 0.1s ease;
}
.section-header.collapsed .codicon {
transform: rotate(-90deg);
}
.section-content {
max-height: 10000px;
overflow: hidden;
transition: max-height 0.15s ease;
}
.section-content.collapsed {
max-height: 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: var(--vscode-descriptionForeground);
text-align: center;
font-size: 14px;
}
.empty-state-section {
padding: 16px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-size: 13px;
opacity: 0.8;
}
.pattern-item {
padding: 8px 12px;
cursor: pointer;
border-radius: 3px;
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.pattern-item:hover {
background: var(--vscode-list-hoverBackground);
}
.pattern-item:hover .pattern-actions {
opacity: 1;
}
.pattern-item.selected {
background: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.pattern-info {
flex: 1;
min-width: 0;
}
.pattern-name {
font-weight: 500;
margin-bottom: 2px;
}
.pattern-preview {
font-size: 12px;
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pattern-item-invalid {
border-left: 2px solid var(--vscode-errorForeground);
opacity: 0.8;
}
.pattern-item-invalid .pattern-name .codicon-warning {
color: var(--vscode-errorForeground);
margin-left: 4px;
font-size: 13px;
vertical-align: middle;
}
.pattern-item-invalid .pattern-preview {
color: var(--vscode-errorForeground);
}
.pattern-item-invalid .pattern-preview em {
font-style: italic;
opacity: 0.7;
}
/* Action buttons */
.pattern-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.15s ease;
}
.action-btn {
padding: 4px;
background: transparent;
border: none;
cursor: pointer;
color: var(--vscode-foreground);
opacity: 0.7;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.action-btn:hover {
background: var(--vscode-toolbar-hoverBackground);
opacity: 1;
}
.action-btn .codicon {
font-size: 16px;
}
.action-btn.delete:hover {
color: var(--vscode-errorForeground);
}
.action-btn.load:hover {
color: var(--vscode-textLink-foreground);
}
/* Right panel - Edit form */
.edit-panel {
flex: 1;
background: var(--vscode-editor-background);
overflow-y: auto;
padding: 16px;
}
.edit-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
opacity: 0.5;
text-align: center;
padding: 40px 20px;
}
.edit-empty-state .codicon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.6;
}
.edit-empty-state p {
margin: 4px 0;
font-size: 14px;
}
.edit-empty-state .hint {
font-size: 12px;
opacity: 0.7;
}
.edit-form {
display: none;
}
.edit-form.active {
display: block;
}
.edit-form-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.edit-form h2 {
font-size: 13px;
font-weight: 600;
margin: 0;
text-transform: uppercase;
opacity: 0.8;
white-space: nowrap;
}
.form-input-inline {
flex: 1;
padding: 4px 6px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
font-size: 13px;
font-family: var(--vscode-editor-font-family);
box-sizing: border-box;
}
.form-input-inline:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.form-group {
margin-bottom: 12px;
}
.form-label {
display: block;
margin-bottom: 3px;
font-weight: 500;
font-size: 11px;
opacity: 0.8;
text-transform: uppercase;
}
.form-label-with-options {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 3px;
}
.form-label-with-options .form-label {
margin-bottom: 0;
}
.form-input,
.form-textarea {
width: 100%;
padding: 4px 6px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
font-size: 13px;
font-family: var(--vscode-editor-font-family);
box-sizing: border-box;
}
.form-input:focus,
.form-textarea:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.form-input.error,
.form-textarea.error {
border-color: var(--vscode-errorForeground);
}
.form-input.dirty,
.form-input-inline.dirty,
.form-textarea.dirty {
box-shadow: 0 0 0 1px rgba(106, 153, 85, 0.6);
}
.form-textarea {
min-height: 60px;
resize: vertical;
font-family: var(--vscode-editor-font-family);
}
.validation-error {
display: none;
color: var(--vscode-errorForeground);
font-size: 11px;
margin-top: 3px;
}
.validation-error.visible {
display: block;
}
.checkbox-group-icons {
display: flex;
gap: 4px;
}
.toggle-btn {
padding: 4px 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid transparent;
border-radius: 2px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
min-width: 28px;
height: 24px;
}
.toggle-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.toggle-btn.active {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-color: var(--vscode-focusBorder);
}
.toggle-btn.dirty {
border-color: #6a9955;
}
.toggle-btn.active.dirty {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border-color: #6a9955;
}
.toggle-btn .codicon {
font-size: 14px;
}
.button-group {
display: flex;
gap: 6px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--vscode-panel-border);
}
button {
padding: 5px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 12px;
}
button:hover {
background: var(--vscode-button-hoverBackground);
}
button.secondary {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
button.secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.dirty-indicator {
margin-left: 6px;
color: var(--vscode-editorWarning-foreground);
font-size: 8px;
animation: pulse 2s ease-in-out infinite;
width: 8px;
display: inline-block;
visibility: hidden;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes pulseBorder {
0%, 100% {
border-color: #4ec9b0;
}
50% {
border-color: #dcdcaa;
}
}
@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<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'; style-src {{cspSource}} https://unpkg.com 'nonce-{{nonce}}'; font-src https://unpkg.com; script-src 'nonce-{{nonce}}';">
<title>Manage Patterns</title>
<link href="https://unpkg.com/@vscode/codicons/dist/codicon.css" rel="stylesheet" />
{{styleTag}}
</head>
<body>
<div class="container">
<!-- Left: Pattern List -->
<div class="pattern-list-panel">
<div class="search-box">
<div class="search-wrapper">
<i class="codicon codicon-search search-icon"></i>
<input type="text" class="search-input" placeholder="Search patterns..." id="searchInput" />
<button class="search-clear" id="searchClear" title="Clear search">
<i class="codicon codicon-close"></i>
</button>
</div>
</div>
<div class="pattern-list" id="patternList">
<!-- Patterns will be inserted here -->
<div class="empty-state">No patterns yet</div>
</div>
</div>
<!-- Resize Handle -->
<div class="resize-handle" id="resizeHandle"></div>
<div class="edit-panel" id="editPanel">
<div class="edit-empty-state" id="editEmptyState">
<i class="codicon codicon-list-selection"></i>
<p>Select a pattern to edit</p>
<p class="hint">or create a new one with the + button</p>
</div>
<div class="edit-form" id="editForm">
<div class="edit-form-header">
<h2>Pattern</h2>
<input type="text" class="form-input-inline" id="labelInput" placeholder="Pattern name" />
</div>
<div class="form-group">
<div class="form-label-with-options">
<label class="form-label" for="findInput">Find</label>
<div class="checkbox-group-icons">
<button type="button" class="toggle-btn" id="isCaseSensitiveBtn" title="Match Case">
<i class="codicon codicon-case-sensitive"></i>
</button>
<button type="button" class="toggle-btn" id="matchWholeWordBtn" title="Match Whole Word">
<i class="codicon codicon-whole-word"></i>
</button>
<button type="button" class="toggle-btn" id="isRegexBtn" title="Use Regular Expression">
<i class="codicon codicon-regex"></i>
</button>
</div>
</div>
<textarea class="form-textarea" id="findInput" placeholder="Search pattern or regex"></textarea>
<div class="validation-error" id="findValidationError"></div>
<!-- Hidden checkboxes to maintain state -->
<input type="checkbox" id="isCaseSensitive" hidden />
<input type="checkbox" id="matchWholeWord" hidden />
<input type="checkbox" id="isRegex" hidden />
</div>
<div class="form-group">
<label class="form-label" for="replaceInput">Replace (optional)</label>
<textarea class="form-textarea" id="replaceInput" placeholder="Replacement text"></textarea>
</div>
<div class="form-group">
<label class="form-label" for="filesToInclude">Files to Include</label>
<input type="text" class="form-input" id="filesToInclude" placeholder="*.ts,*.js or src/**" />
</div>
<div class="form-group">
<label class="form-label" for="filesToExclude">Files to Exclude</label>
<input type="text" class="form-input" id="filesToExclude" placeholder="node_modules/**,dist/**" />
</div>
<div class="button-group">
<button id="saveBtn">
Save
<span id="dirtyIndicator" class="dirty-indicator">
<i class="codicon codicon-circle-filled"></i>
</span>
</button>
<button id="loadBtn" class="secondary">
<i class="codicon codicon-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
<script nonce="{{nonce}}">{{script}}</script>
</body>
</html>
@@ -0,0 +1,857 @@
const vscode = acquireVsCodeApi();
// Store patterns data
let workspacePatterns = [];
let userPatterns = [];
let workspacePatternsMap = new Map(); // id -> pattern
let userPatternsMap = new Map(); // id -> pattern
let searchQuery = '';
// Dirty state tracking
let savedPatternState = null;
// Cache DOM elements for performance
const domCache = {
labelInput: null,
findInput: null,
replaceInput: null,
filesToInclude: null,
filesToExclude: null,
isRegex: null,
isCaseSensitive: null,
matchWholeWord: null,
isRegexBtn: null,
isCaseSensitiveBtn: null,
matchWholeWordBtn: null,
dirtyIndicator: null,
patternList: null,
editForm: null,
editEmptyState: null,
findValidationError: null
};
function initDomCache() {
domCache.labelInput = document.getElementById('labelInput');
domCache.findInput = document.getElementById('findInput');
domCache.replaceInput = document.getElementById('replaceInput');
domCache.filesToInclude = document.getElementById('filesToInclude');
domCache.filesToExclude = document.getElementById('filesToExclude');
domCache.isRegex = document.getElementById('isRegex');
domCache.isCaseSensitive = document.getElementById('isCaseSensitive');
domCache.matchWholeWord = document.getElementById('matchWholeWord');
domCache.isRegexBtn = document.getElementById('isRegexBtn');
domCache.isCaseSensitiveBtn = document.getElementById('isCaseSensitiveBtn');
domCache.matchWholeWordBtn = document.getElementById('matchWholeWordBtn');
domCache.dirtyIndicator = document.getElementById('dirtyIndicator');
domCache.patternList = document.getElementById('patternList');
domCache.editForm = document.getElementById('editForm');
domCache.editEmptyState = document.getElementById('editEmptyState');
domCache.findValidationError = document.getElementById('findValidationError');
}
function checkIfDirty() {
const state = vscode.getState() || {};
const currentPattern = state.currentPattern;
if (!currentPattern || !savedPatternState) {
updateDirtyIndicator(false);
clearAllDirtyClasses();
return;
}
// Compare current form values with saved state
const isDirty =
domCache.labelInput.value.trim() !== savedPatternState.label ||
domCache.findInput.value !== savedPatternState.find ||
domCache.replaceInput.value !== savedPatternState.replace ||
domCache.isRegex.checked !== savedPatternState.flags.isRegex ||
domCache.isCaseSensitive.checked !== savedPatternState.flags.isCaseSensitive ||
domCache.matchWholeWord.checked !== savedPatternState.flags.matchWholeWord ||
(domCache.filesToInclude.value.trim() || '') !== (savedPatternState.filesToInclude || '') ||
(domCache.filesToExclude.value.trim() || '') !== (savedPatternState.filesToExclude || '');
updateDirtyIndicator(isDirty);
updateFieldDirtyStates();
}
function updateDirtyIndicator(isDirty) {
if (domCache.dirtyIndicator) {
domCache.dirtyIndicator.style.visibility = isDirty ? 'visible' : 'hidden';
}
}
function updateFieldDirtyStates() {
if (!savedPatternState) {
clearAllDirtyClasses();
return;
}
// Check text inputs using cached DOM references
if (domCache.labelInput) {
toggleDirtyClass(domCache.labelInput, domCache.labelInput.value.trim() !== savedPatternState.label);
}
if (domCache.findInput) {
toggleDirtyClass(domCache.findInput, domCache.findInput.value !== savedPatternState.find);
}
if (domCache.replaceInput) {
toggleDirtyClass(domCache.replaceInput, domCache.replaceInput.value !== savedPatternState.replace);
}
if (domCache.filesToInclude) {
toggleDirtyClass(domCache.filesToInclude, (domCache.filesToInclude.value.trim() || '') !== (savedPatternState.filesToInclude || ''));
}
if (domCache.filesToExclude) {
toggleDirtyClass(domCache.filesToExclude, (domCache.filesToExclude.value.trim() || '') !== (savedPatternState.filesToExclude || ''));
}
// Check toggle buttons using cached DOM references
if (domCache.isRegexBtn && domCache.isRegex) {
toggleDirtyClass(domCache.isRegexBtn, domCache.isRegex.checked !== savedPatternState.flags.isRegex);
}
if (domCache.isCaseSensitiveBtn && domCache.isCaseSensitive) {
toggleDirtyClass(domCache.isCaseSensitiveBtn, domCache.isCaseSensitive.checked !== savedPatternState.flags.isCaseSensitive);
}
if (domCache.matchWholeWordBtn && domCache.matchWholeWord) {
toggleDirtyClass(domCache.matchWholeWordBtn, domCache.matchWholeWord.checked !== savedPatternState.flags.matchWholeWord);
}
}
function toggleDirtyClass(element, isDirty) {
if (isDirty) {
element.classList.add('dirty');
} else {
element.classList.remove('dirty');
}
}
function clearAllDirtyClasses() {
const elements = [
domCache.labelInput, domCache.findInput, domCache.replaceInput,
domCache.filesToInclude, domCache.filesToExclude,
domCache.isRegexBtn, domCache.isCaseSensitiveBtn, domCache.matchWholeWordBtn
];
elements.forEach(element => {
if (element) {
element.classList.remove('dirty');
}
});
}
function saveDirtyState(pattern) {
savedPatternState = {
label: pattern.label,
find: pattern.find || '',
replace: pattern.replace || '',
flags: { ...pattern.flags },
filesToInclude: pattern.filesToInclude || '',
filesToExclude: pattern.filesToExclude || ''
};
updateDirtyIndicator(false);
clearAllDirtyClasses();
}
function findPatternById(id, scope) {
const map = scope === 'workspace' ? workspacePatternsMap : userPatternsMap;
return map.get(id);
}
function rebuildPatternMaps() {
workspacePatternsMap.clear();
userPatternsMap.clear();
workspacePatterns.forEach(p => workspacePatternsMap.set(p.id, p));
userPatterns.forEach(p => userPatternsMap.set(p.id, p));
}
// Get collapse state from localStorage
const getCollapseState = (section) => {
const state = vscode.getState() || {};
return state[section + '_collapsed'] === true;
};
// Get collapse state with a default value if not previously set
const getCollapseStateWithDefault = (section, defaultValue) => {
const state = vscode.getState() || {};
const key = section + '_collapsed';
// If user has explicitly set a preference, use it; otherwise use default
return state[key] !== undefined ? state[key] : defaultValue;
};
// Save collapse state to localStorage
const setCollapseState = (section, collapsed) => {
const state = vscode.getState() || {};
state[section + '_collapsed'] = collapsed;
vscode.setState(state);
};
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'patterns') {
disablePatternList(false);
document.body.style.cursor = '';
workspacePatterns = message.workspace;
userPatterns = message.user;
rebuildPatternMaps(); // Rebuild Maps for O(1) lookups
renderPatternList();
setupSearchInput();
// Force clear selection if requested (e.g., on external config changes)
if (message.clearSelection) {
clearDetailsView();
} else if (message.selectPattern) {
selectPattern(message.selectPattern.id, message.selectPattern.scope);
} else {
const state = vscode.getState() || {};
if (state.currentPattern) {
const allPatterns = state.currentPattern.scope === 'workspace' ? workspacePatterns : userPatterns;
const stillExists = allPatterns.some(p => p.id === state.currentPattern.id);
if (!stillExists) {
clearDetailsView();
}
}
}
// Focus search input when webview opens
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
}
});
function setupSearchInput() {
const searchInput = document.getElementById('searchInput');
const searchClear = document.getElementById('searchClear');
if (!searchInput || !searchClear) return;
// if (searchInput.dataset.initialized === 'true') {
// return;
// }
// searchInput.dataset.initialized = 'true';
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase();
if (searchQuery) {
searchClear.classList.add('visible');
} else {
searchClear.classList.remove('visible');
}
renderPatternList();
});
searchClear.addEventListener('click', () => {
searchInput.value = '';
searchQuery = '';
searchClear.classList.remove('visible');
renderPatternList();
searchInput.focus();
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && searchQuery) {
searchInput.value = '';
searchQuery = '';
searchClear.classList.remove('visible');
renderPatternList();
}
});
}
function filterPatterns(patterns) {
if (!searchQuery) return patterns;
return patterns.filter(pattern => {
const nameMatch = pattern.label.toLowerCase().includes(searchQuery);
const findMatch = pattern.find.toLowerCase().includes(searchQuery);
const replaceMatch = pattern.replace && pattern.replace.toLowerCase().includes(searchQuery);
return nameMatch || findMatch || replaceMatch;
});
}
function renderPatternList() {
const listContainer = document.getElementById('patternList');
const filteredWorkspace = filterPatterns(workspacePatterns);
const filteredUser = filterPatterns(userPatterns);
let html = '';
if (searchQuery && filteredWorkspace.length === 0 && filteredUser.length === 0) {
listContainer.innerHTML = '<div class="empty-state">No patterns found</div>';
return;
}
// Collapse empty sections by default, but respect user's saved preference
const workspaceCollapsed = searchQuery ? false : getCollapseStateWithDefault('workspace', filteredWorkspace.length === 0);
html += renderSection('workspace', 'Workspace', filteredWorkspace, workspaceCollapsed);
const userCollapsed = searchQuery ? false : getCollapseStateWithDefault('user', filteredUser.length === 0);
html += renderSection('user', 'User', filteredUser, userCollapsed);
listContainer.innerHTML = html;
document.querySelectorAll('.section-header').forEach(header => {
header.addEventListener('click', toggleSection);
});
// Event delegation handles all pattern list interactions, no need to re-attach
}
function renderSection(id, title, patterns, collapsed) {
const chevron = collapsed ? 'codicon-chevron-right' : 'codicon-chevron-down';
const contentClass = collapsed ? 'section-content collapsed' : 'section-content';
let html = `
<div class="section-header ${collapsed ? 'collapsed' : ''}" data-section="${id}">
<div class="section-header-left">
<i class="codicon ${chevron}"></i>
<span>${title} (${patterns.length})</span>
</div>
<button class="add-pattern-btn" data-scope="${id}" title="Add new pattern">
<i class="codicon codicon-add"></i>
</button>
</div>
<div class="${contentClass}" data-section-content="${id}">
`;
if (patterns.length === 0) {
html += '<div class="empty-state-section">No patterns in this scope</div>';
} else {
patterns.forEach(pattern => {
html += renderPatternItem(pattern);
});
}
html += '</div>';
return html;
}
function toggleSection(event) {
if (event.target.closest('.add-pattern-btn')) {
return;
}
const header = event.currentTarget;
const sectionId = header.dataset.section;
const content = document.querySelector('[data-section-content="' + sectionId + '"]');
const chevron = header.querySelector('.codicon');
const isCollapsed = header.classList.contains('collapsed');
if (isCollapsed) {
header.classList.remove('collapsed');
content.classList.remove('collapsed');
chevron.classList.remove('codicon-chevron-right');
chevron.classList.add('codicon-chevron-down');
setCollapseState(sectionId, false);
} else {
header.classList.add('collapsed');
content.classList.add('collapsed');
chevron.classList.remove('codicon-chevron-down');
chevron.classList.add('codicon-chevron-right');
setCollapseState(sectionId, true);
}
}
function renderPatternItem(pattern) {
const preview = pattern.find.substring(0, 40);
const truncated = pattern.find.length > 40 ? '...' : '';
const isInvalid = !pattern.find || pattern.find.trim() === '';
const invalidClass = isInvalid ? 'pattern-item-invalid' : '';
return `
<div class="pattern-item ${invalidClass}" data-id="${pattern.id}" data-label="${pattern.label}" data-scope="${pattern.scope}"> <div class="pattern-info">
<div class="pattern-name">
${pattern.label}
${isInvalid ? '<i class="codicon codicon-warning" title="Find field is empty"></i>' : ''}
</div>
<div class="pattern-preview">${isInvalid ? '<em>Empty find pattern</em>' : preview + truncated}</div>
</div>
<div class="pattern-actions">
${renderActionButton('trash', 'delete', 'Delete pattern')}
${renderActionButton('arrow-right', 'load', 'Load to search')}
</div>
</div>
`;
}
function renderActionButton(icon, action, title) {
return `
<button class="action-btn ${action}"
data-action="${action}"
title="${title}"
onclick="event.stopPropagation()">
<i class="codicon codicon-${icon}"></i>
</button>
`;
}
// Event delegation for pattern list - set up once, works for all items
function setupEventDelegation() {
if (!domCache.patternList) return;
// Delegate all pattern list events to the parent container
domCache.patternList.addEventListener('click', (event) => {
// Handle pattern item click
const patternItem = event.target.closest('.pattern-item');
if (patternItem && !event.target.closest('.action-btn') && !event.target.closest('.add-pattern-btn')) {
handlePatternClick(patternItem);
return;
}
// Handle action button clicks (load, delete)
const actionBtn = event.target.closest('.action-btn');
if (actionBtn) {
event.stopPropagation();
handleActionClick(actionBtn);
return;
}
// Handle add pattern button clicks
const addBtn = event.target.closest('.add-pattern-btn');
if (addBtn) {
event.stopPropagation();
handleAddPattern(addBtn);
return;
}
});
}
function handlePatternClick(item) {
const id = item.dataset.id;
const scope = item.dataset.scope;
document.querySelectorAll('.pattern-item').forEach(i => {
i.classList.remove('selected');
});
item.classList.add('selected');
const pattern = findPatternById(id, scope);
if (pattern) {
populateDetailsView(pattern);
}
}
function populateDetailsView(pattern) {
domCache.editEmptyState.style.display = 'none';
domCache.editForm.classList.add('active');
domCache.labelInput.value = pattern.label;
domCache.findInput.value = pattern.find || '';
domCache.replaceInput.value = pattern.replace || '';
domCache.isRegex.checked = pattern.flags.isRegex;
domCache.isCaseSensitive.checked = pattern.flags.isCaseSensitive;
domCache.matchWholeWord.checked = pattern.flags.matchWholeWord;
updateToggleButtonState(domCache.isRegexBtn, pattern.flags.isRegex);
updateToggleButtonState(domCache.isCaseSensitiveBtn, pattern.flags.isCaseSensitive);
updateToggleButtonState(domCache.matchWholeWordBtn, pattern.flags.matchWholeWord);
domCache.findInput.classList.remove('error');
domCache.findValidationError.classList.remove('visible');
validateRegexPattern();
domCache.filesToInclude.value = pattern.filesToInclude || '';
domCache.filesToExclude.value = pattern.filesToExclude || '';
const state = vscode.getState() || {};
state.currentPattern = {
id: pattern.id,
label: pattern.label,
scope: pattern.scope
};
vscode.setState(state);
// Save the clean state for dirty tracking
saveDirtyState(pattern);
}
function selectPattern(id, scope) {
const item = document.querySelector(
`.pattern-item[data-id="${id}"][data-scope="${scope}"]`
);
if (item) {
item.click();
}
}
function handleSavePattern() {
const state = vscode.getState() || {};
const currentPattern = state.currentPattern;
if (!currentPattern) {
console.error('No pattern selected to save');
return;
}
const label = domCache.labelInput.value.trim();
const find = domCache.findInput.value;
const replace = domCache.replaceInput.value;
if (!label) {
alert('Pattern name cannot be empty');
return;
}
disablePatternList(true);
document.body.style.cursor = 'wait';
const pattern = {
id: currentPattern.id,
label: label,
find: find,
replace: replace,
flags: {
isRegex: domCache.isRegex.checked,
isCaseSensitive: domCache.isCaseSensitive.checked,
matchWholeWord: domCache.matchWholeWord.checked,
isMultiline: false
},
filesToInclude: domCache.filesToInclude.value.trim() || undefined,
filesToExclude: domCache.filesToExclude.value.trim() || undefined,
scope: currentPattern.scope
};
vscode.postMessage({
type: 'save',
pattern: pattern
});
}
function disablePatternList(disabled) {
const listPanel = document.querySelector('.pattern-list-panel');
if (disabled) {
listPanel.style.pointerEvents = 'none';
listPanel.style.opacity = '0.6';
} else {
listPanel.style.pointerEvents = '';
listPanel.style.opacity = '';
}
}
function handleDeletePattern() {
const state = vscode.getState() || {};
const currentPattern = state.currentPattern;
if (!currentPattern) {
console.error('No pattern selected to delete');
return;
}
vscode.postMessage({
type: 'delete',
id: currentPattern.id,
scope: currentPattern.scope
});
}
function handleLoadPatternFromDetails() {
const state = vscode.getState() || {};
const currentPattern = state.currentPattern;
if (!currentPattern) {
console.error('No pattern selected to load');
return;
}
// Build pattern from current form values (may be dirty)
const pattern = {
id: currentPattern.id,
label: domCache.labelInput.value.trim(),
find: domCache.findInput.value,
replace: domCache.replaceInput.value,
flags: {
isRegex: domCache.isRegex.checked,
isCaseSensitive: domCache.isCaseSensitive.checked,
matchWholeWord: domCache.matchWholeWord.checked,
isMultiline: false
},
filesToInclude: domCache.filesToInclude.value.trim() || undefined,
filesToExclude: domCache.filesToExclude.value.trim() || undefined,
scope: currentPattern.scope
};
vscode.postMessage({
type: 'load',
id: currentPattern.id,
scope: currentPattern.scope,
pattern: pattern
});
}
function clearDetailsView() {
domCache.editForm.classList.remove('active');
domCache.editEmptyState.style.display = 'flex';
domCache.labelInput.value = '';
domCache.findInput.value = '';
domCache.replaceInput.value = '';
domCache.isRegex.checked = false;
domCache.isCaseSensitive.checked = false;
domCache.matchWholeWord.checked = false;
domCache.filesToInclude.value = '';
domCache.filesToExclude.value = '';
const state = vscode.getState() || {};
delete state.currentPattern;
vscode.setState(state);
document.querySelectorAll('.pattern-item').forEach(i => {
i.classList.remove('selected');
});
// Clear dirty state
savedPatternState = null;
updateDirtyIndicator(false);
clearAllDirtyClasses();
}
/**
* Setup toggle buttons for flags
*/
function setupToggleButtons() {
const buttons = [
{ btnId: 'isRegexBtn', checkboxId: 'isRegex' },
{ btnId: 'isCaseSensitiveBtn', checkboxId: 'isCaseSensitive' },
{ btnId: 'matchWholeWordBtn', checkboxId: 'matchWholeWord' }
];
buttons.forEach(({ btnId, checkboxId }) => {
const btn = document.getElementById(btnId);
const checkbox = document.getElementById(checkboxId);
if (!btn || !checkbox) return;
btn.addEventListener('click', () => {
checkbox.checked = !checkbox.checked;
updateToggleButtonState(btn, checkbox.checked);
// Validate regex when regex flag changes
if (checkboxId === 'isRegex') {
validateRegexPattern();
}
// Check if field is dirty
checkIfDirty();
});
// Initialize button state
updateToggleButtonState(btn, checkbox.checked);
});
}
/**
* Update visual state of toggle button
*/
function updateToggleButtonState(button, isActive) {
if (isActive) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
}
/**
* Validate regex pattern
*/
function validateRegexPattern() {
const isRegex = domCache.isRegex.checked;
if (!isRegex) {
// Not using regex, clear any errors
domCache.findInput.classList.remove('error');
domCache.findValidationError.classList.remove('visible');
domCache.findValidationError.textContent = '';
return true;
}
const pattern = domCache.findInput.value;
if (!pattern) {
// Empty pattern is okay
domCache.findInput.classList.remove('error');
domCache.findValidationError.classList.remove('visible');
domCache.findValidationError.textContent = '';
return true;
}
try {
new RegExp(pattern);
// Valid regex
domCache.findInput.classList.remove('error');
domCache.findValidationError.classList.remove('visible');
domCache.findValidationError.textContent = '';
return true;
} catch (e) {
// Invalid regex
domCache.findInput.classList.add('error');
domCache.findValidationError.classList.add('visible');
domCache.findValidationError.textContent = `Invalid regular expression: ${e.message}`;
return false;
}
}
function handleAddPattern(button) {
const scope = button.dataset.scope;
// Visual feedback: disable button temporarily
button.disabled = true;
button.style.opacity = '0.5';
button.style.cursor = 'wait';
vscode.postMessage({
type: 'create',
scope: scope === 'workspace' ? 'workspace' : 'global'
});
// Re-enable after 2 seconds (fallback in case of error)
setTimeout(() => {
button.disabled = false;
button.style.opacity = '';
button.style.cursor = '';
}, 2000);
}
function handleActionClick(button) {
const action = button.dataset.action;
const item = button.closest('.pattern-item');
const id = item.dataset.id;
const scope = item.dataset.scope;
if (action === 'delete') {
handleDelete(id, scope);
} else if (action === 'load') {
handleLoad(id, scope);
}
}
function handleDelete(id, scope) {
vscode.postMessage({
type: 'delete',
id: id,
scope: scope
});
}
function handleLoad(id, scope) {
if (!id || !scope) {
console.error('Pattern not found');
return;
}
vscode.postMessage({
type: 'load',
id: id,
scope: scope
});
}
document.getElementById('saveBtn').addEventListener('click', handleSavePattern);
document.getElementById('loadBtn').addEventListener('click', handleLoadPatternFromDetails);
setupToggleButtons();
// Add event listeners to all form inputs for dirty tracking
const formInputs = [
'labelInput', 'findInput', 'replaceInput',
'isRegex', 'isCaseSensitive', 'matchWholeWord',
'filesToInclude', 'filesToExclude'
];
formInputs.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('input', checkIfDirty);
element.addEventListener('change', checkIfDirty);
}
});
// Initialize DOM cache for performance
initDomCache();
// Set up event delegation for pattern list (once, not per render)
setupEventDelegation();
// Resizable panels functionality
function setupResizablePanels() {
const resizeHandle = document.getElementById('resizeHandle');
const patternListPanel = document.querySelector('.pattern-list-panel');
if (!resizeHandle || !patternListPanel) return;
let isResizing = false;
let startX = 0;
let startWidth = 0;
// Load saved width from state
const state = vscode.getState() || {};
if (state.panelWidth) {
patternListPanel.style.flexBasis = `${state.panelWidth}px`;
}
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startWidth = patternListPanel.offsetWidth;
resizeHandle.classList.add('resizing');
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const newWidth = startWidth + deltaX;
// Respect min/max constraints
const minWidth = 200;
const maxWidth = 600;
const clampedWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
patternListPanel.style.flexBasis = `${clampedWidth}px`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
resizeHandle.classList.remove('resizing');
document.body.style.cursor = '';
document.body.style.userSelect = '';
// Save width to state
const currentWidth = patternListPanel.offsetWidth;
const state = vscode.getState() || {};
state.panelWidth = currentWidth;
vscode.setState(state);
}
});
}
// Set up resizable panels
setupResizablePanels();
// Add validation listener to find input
if (domCache.findInput) {
domCache.findInput.addEventListener('input', () => {
validateRegexPattern();
});
}
vscode.postMessage({ type: 'ready' });