Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project configuration improvements #103

Merged
merged 10 commits into from
Sep 10, 2024
14 changes: 7 additions & 7 deletions src/extension/editors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export abstract class BaseEditor extends BaseWebview<EditorWebviewArgs> implemen
}
})
);
disposables.push(
vscode.workspace.onDidDeleteFiles((event) => {
if (event.files.map((uri) => uri.toString()).includes(document.uri.toString())) {
void webviewPanel.dispose();
}
})
);
disposables.push(
vscode.workspace.onDidSaveTextDocument((event) => {
if (event.uri.toString() === document.uri.toString()) {
Expand All @@ -53,13 +60,6 @@ export abstract class BaseEditor extends BaseWebview<EditorWebviewArgs> implemen
})
);

// Create file system watcher
const watcher = vscode.workspace.createFileSystemWatcher(document.uri.fsPath);
watcher.onDidCreate(() => this.update(document, webview, true));
watcher.onDidChange(() => this.update(document, webview, true));
watcher.onDidDelete(() => this.update(document, webview, true));
disposables.push(watcher);

// Add dispose listener
webviewPanel.onDidDispose(() => {
this.onClose(document, webview);
Expand Down
32 changes: 22 additions & 10 deletions src/extension/editors/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import type {GlobalStoreMessage, ViewMessage} from '../types.js';
import {BaseEditor, type EditorWebviewArgs} from './base.js';

export class ProjectEditor extends BaseEditor {
private static readonly SAVE_DEBOUNCE_WAIT = 1000;
private saveDebounceTimer: ReturnType<typeof setTimeout> | undefined;

private doIgnoreSave = false;

public static getViewType() {
return 'edacation.project';
}
Expand All @@ -32,6 +37,12 @@ export class ProjectEditor extends BaseEditor {
}
}

private async whileIgnoreSave(callback: () => Promise<unknown>): Promise<void> {
this.doIgnoreSave = true;
await callback();
this.doIgnoreSave = false;
}

protected async onDidReceiveMessage(
document: vscode.TextDocument,
webview: vscode.Webview,
Expand All @@ -57,11 +68,16 @@ export class ProjectEditor extends BaseEditor {
return true;
}

const edit = new vscode.WorkspaceEdit();
edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), message.document);
await vscode.workspace.applyEdit(edit);
await this.whileIgnoreSave(async () => {
const edit = new vscode.WorkspaceEdit();
edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), message.document);
await vscode.workspace.applyEdit(edit);
});

await document.save();
if (this.saveDebounceTimer) clearTimeout(this.saveDebounceTimer);
this.saveDebounceTimer = setTimeout(() => {
void this.whileIgnoreSave(async () => await document.save());
}, ProjectEditor.SAVE_DEBOUNCE_WAIT);

return true;
}
Expand All @@ -80,12 +96,8 @@ export class ProjectEditor extends BaseEditor {
// Do nothing
}

protected async update(document: vscode.TextDocument, webview: vscode.Webview, isDocumentChange: boolean) {
if (isDocumentChange) {
return;
}

await vscode.commands.executeCommand('edacation-projects.focus');
protected async update(document: vscode.TextDocument, webview: vscode.Webview, _isDocumentChange: boolean) {
if (this.doIgnoreSave) return;

const project = this.projects.get(document.uri);

Expand Down
6 changes: 4 additions & 2 deletions src/views/project/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import {vscode} from '../../vscode';

import EDAProject from './components/EDAProject.vue';
import {state} from './state';
import {ignoreSave, state} from './state';

provideVSCodeDesignSystem().register(
vsCodeButton(),
Expand Down Expand Up @@ -55,7 +55,9 @@ export default {

switch (event.data.type) {
case 'project':
this.state.project = event.data.project;
ignoreSave(() => {
this.state.project = event.data.project;
})
break;
}
}
Expand Down
96 changes: 72 additions & 24 deletions src/views/project/src/components/EDAProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ import {defineComponent} from 'vue';
import {state as globalState} from '../state';

import EDATarget from './EDATarget.vue';
import type { TargetConfiguration } from 'edacation';

export default defineComponent({
components: {
EDATarget
},
computed: {
targetIndex(): number | undefined {
console.log(
'target index',
this.state.selectedTargetIndex,
this.state.selectedTargetIndex === 'all' ? undefined : parseInt(this.state.selectedTargetIndex)
);
return this.state.selectedTargetIndex === 'all' ? undefined : parseInt(this.state.selectedTargetIndex);
targetIndex: {
get(): number | undefined {
if (this.targets.length === 0) return undefined;

let selectedIndex = Number(this.state.selectedTargetIndex ?? 0);
if (selectedIndex < 0) selectedIndex = 0;
if (selectedIndex >= this.targets.length) selectedIndex = this.targets.length - 1;
return selectedIndex;
},
set(index: number) {
this.state.selectedTargetIndex = index;
}
},
targets(): TargetConfiguration[] {
return this.state.project?.configuration.targets ?? [];
}
},
data() {
Expand All @@ -25,29 +34,46 @@ export default defineComponent({
};
},
methods: {
handleNameChange(event: Event) {
if (!this.state.project || !event.target) {
return;
getNewTargetId(): string {
const takenIds = this.targets.map(target => target.id);

let index = this.targets.length + 1;
while (takenIds.includes(`target${index}`)) index++;

return `target${index}`;
},
getDuplicateTargetId(oldId: string): string {
const match = oldId.match(/^(.*)(\d)+$/)
let base: string;
let seq: number;
if (match) {
base = match[1];
seq = Number(match[2]) + 1;
} else {
base = oldId;
seq = 1;
}

this.state.project.name = (event.target as HTMLInputElement).value;
const takenIds = this.targets.map(target => target.id);
while (takenIds.includes(`${base}${seq}`)) seq++;

return `${base}${seq}`;
},
handleTargetChange(event: Event) {
if (!event.target) {
handleNameChange(event: Event) {
if (!this.state.project || !event.target) {
return;
}

this.state.selectedTargetIndex = (event.target as HTMLSelectElement).value;
this.state.project.name = (event.target as HTMLInputElement).value;
},
handleTargetAdd() {
if (!this.state.project) {
return;
}

const index = this.state.project.configuration.targets.length + 1;
this.state.project.configuration.targets.push({
id: `target${index}`,
name: `Target ${index}`,
id: this.getNewTargetId(),
name: `Target ${this.targets.length + 1}`,
vendor: 'generic',
family: 'generic',
device: 'generic',
Expand All @@ -56,13 +82,30 @@ export default defineComponent({
// TODO: This does not work, because does not yet exist, due to sync issues
// this.state.selectedTargetIndex = (index - 1).toString();
},
handleTargetDuplicate() {
const targetIndex = this.targetIndex;
if (!this.state.project || targetIndex === undefined) {
return;
}

const curTarget = this.targets[targetIndex];
if (!curTarget) return;

this.state.project.configuration.targets.push({
...curTarget,
id: this.getDuplicateTargetId(curTarget.id),
name: `Target ${this.targets.length + 1}`
});
// TODO: This does not work, because does not yet exist, due to sync issues
// this.state.selectedTargetIndex = (index - 1).toString();
},
handleTargetDelete() {
if (!this.state.project || this.targetIndex === undefined) {
return;
}

this.state.project.configuration.targets.splice(this.targetIndex, 1);
this.state.selectedTargetIndex = 'all';
this.targetIndex = 0;
}
}
});
Expand All @@ -77,26 +120,31 @@ export default defineComponent({

<h1>Targets</h1>
<p>Select target to configure</p>
<vscode-dropdown :value="state.selectedTargetIndex ?? 'all'" @input="handleTargetChange" style="width: 20rem">
<vscode-option value="all">Defaults for all targets</vscode-option>
<vscode-option v-for="(target, index) in state.project.configuration.targets" :key="index" :value="index">
<vscode-dropdown v-model.number="targetIndex" style="width: 20rem">
<vscode-option v-for="(target, index) in targets" :key="index" :value="index">
{{ target.name }}
</vscode-option>
</vscode-dropdown>


<vscode-button v-if="targetIndex !== undefined" style="margin-start: 1rem" @click="handleTargetDelete">
Delete target
</vscode-button>

<div>
<div style="display: flex; gap: 1rem">
<vscode-button style="margin-top: 1rem" @click="handleTargetAdd">Add target</vscode-button>
<vscode-button
v-if="targetIndex !== undefined"
style="margin-top: 1rem"
@click="handleTargetDuplicate"
>Duplicate target</vscode-button>
</div>

<p v-if="state.project.configuration.targets.length === 0"><b>Error:</b> At least one target is required.</p>
<p v-if="targets.length === 0"><b>Error:</b> At least one target is required.</p>

<vscode-divider style="margin-top: 1rem" />

<EDATarget :targetIndex="targetIndex" />
<EDATarget v-if="targetIndex !== undefined" :targetIndex="Number(targetIndex)" />
</template>
<template v-else>
<p>No project configuration available.</p>
Expand Down
46 changes: 33 additions & 13 deletions src/views/project/src/components/EDATargetCheckbox.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<script lang="ts">
import type {
NextpnrConfiguration,
NextpnrOptions,
NextpnrTargetConfiguration,
TargetConfiguration,
WorkerId,
YosysConfiguration,
YosysOptions,
YosysTargetConfiguration
import {
getNextpnrDefaultOptions,
getNextpnrOptions,
getYosysDefaultOptions,
getYosysOptions,
type NextpnrConfiguration,
type NextpnrOptions,
type NextpnrTargetConfiguration,
type TargetConfiguration,
type WorkerId,
type YosysConfiguration,
type YosysOptions,
type YosysTargetConfiguration,
} from 'edacation';
import {type PropType, defineComponent} from 'vue';

Expand Down Expand Up @@ -63,11 +67,27 @@ export default defineComponent({
}
return this.worker.options;
},
config(): boolean | undefined {
if (!this.options) {
effectiveOptions(): YosysOptions | NextpnrOptions | null {
const projectConfig = this.state.project!.configuration;
const targetId = this.target?.id;

if (!targetId) {
// Default configuration
if (this.workerId === 'yosys') return getYosysDefaultOptions(projectConfig)
if (this.workerId === 'nextpnr') return getNextpnrDefaultOptions(projectConfig)
return null;
} else {
// Target configuration
if (this.workerId === 'yosys') return getYosysOptions(projectConfig, targetId)
if (this.workerId === 'nextpnr') return getNextpnrOptions(projectConfig, targetId)
return null;
}
},
effectiveConfig(): boolean | undefined {
if (!this.effectiveOptions) {
return undefined;
}
return this.options[this.configId as keyof typeof this.options];
return this.effectiveOptions[this.configId as keyof typeof this.effectiveOptions];
}
},
methods: {
Expand Down Expand Up @@ -109,6 +129,6 @@ export default defineComponent({

<template>
<div>
<vscode-checkbox :checked="config" @change="handleCheckboxChange">{{ configName }}</vscode-checkbox>
<vscode-checkbox :checked="effectiveConfig" @change="handleCheckboxChange">{{ configName }}</vscode-checkbox>
</div>
</template>
14 changes: 12 additions & 2 deletions src/views/project/src/components/EDATargetGeneral.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ export default defineComponent({
prev[packageId] = vendorPackages[packageId] ?? packageId;
return prev;
}, {} as Record<string, string>);
},
hasIdOverlap(): boolean {
if (!this.target) return false;

const targetIds = this.state.project!.configuration.targets.map(target => target.id);
return targetIds.filter(id => id === this.target?.id).length >= 2;
}
},
data() {
Expand Down Expand Up @@ -154,18 +160,22 @@ export default defineComponent({
margin-bottom: 1rem;
"
>
<vscode-text-field placeholder="ID" :value="target.id" @input="handleIdChange">ID</vscode-text-field>
<vscode-text-field placeholder="ID" :value="target.id" @input="handleIdChange">
ID <span style="margin-inline: 2rem; color: red;" v-if="hasIdOverlap">Error: duplicate ID</span>
</vscode-text-field>

<vscode-text-field placeholder="Name" :value="target.name" @input="handleNameChange">
Name
</vscode-text-field>

<!-- TODO: Make this configurable again
<vscode-text-field
placeholder="Output directory"
:value="target.directory || ''"
@input="handleDirectoryChange"
>Output directory</vscode-text-field
>
> -->
<div></div>

<div></div>

Expand Down
Loading