Skip to content

Commit

Permalink
Merge pull request #103 from EDAcation/feat/better-project-config
Browse files Browse the repository at this point in the history
Project configuration improvements
  • Loading branch information
malmeloo authored Sep 10, 2024
2 parents c8eb980 + c248cf4 commit 8eed377
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 64 deletions.
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

0 comments on commit 8eed377

Please sign in to comment.