0.9.2: Source view, schema checks without the YAML extension
Every Archify page can show its file's YAML in place. Diagrams get Diagram | Graph | Source (Graph only inside a model), main.archify Model | Graph | Source, registry and decision pages View | Source, and the Graph page a Source segment. 2D | 3D moves left of that switch and hides while Source shows. The YAML opens in a CodeMirror editor inside the page (dist/source.js, loaded on first use), themed with the page's colours. Edits go to the VS Code document as WorkspaceEdits; outside changes come back to the page. Undo and redo are the editor's own, since VS Code sends ⌘Z in a focused webview to the webview; save and find stay VS Code's. Problems come from the host only (what the Problems panel holds for the file) and are counted on the Source segment; completion and hover come from the schema, folded into one draft-07 document per diagram type. The host now checks .archify files against the extension's schema with ajv on the YAML as written, before archify validate, and checks .archify-registry files against the registry schema for the first time. Registry and decision pages become text editor providers. While Source shows, new renders wait and appear when the picture comes back. Show Source switches the page to Source; Open in Text Editor (the editor title button) opens the text beside it, as Show Source did. The decision schema is no longer given to the YAML extension. Fixed: YAML errors in registry and decision files were reported on line 1; "Webview is disposed" when a diagram closed mid-render; the model heading under the toolbar in narrow editors.
This commit is contained in:
1 parent
5bcd34b839
commit
27d4794ff8
32 files changed
+2486
-127
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { EditorSchemas } from '../language/editorSchemas';
|
||||
import { readSchemas } from '../language/schemaCheck';
|
||||
import { log } from '../util/log';
|
||||
import { TextChange, normalizeEol, offsetOf, positionOf } from '../util/textDiff';
|
||||
|
||||
/** Page → host messages of the Source view. */
|
||||
export type SourceMessage =
|
||||
| { type: 'sourceReady' }
|
||||
| { type: 'sourceOpen' }
|
||||
| { type: 'sourceClose' }
|
||||
| { type: 'sourceEdit'; base: string; text: string; changes: TextChange[] };
|
||||
|
||||
export type SourceKind = 'diagram' | 'registry' | 'decision';
|
||||
|
||||
const CHECKS: Record<SourceKind, string> = {
|
||||
diagram: 'YAML · checked against the Archify schema and the model',
|
||||
registry: 'YAML · checked against the registry schema and the model',
|
||||
decision: 'YAML · checked as a decision of the model',
|
||||
};
|
||||
|
||||
let editorSchemas: EditorSchemas | undefined;
|
||||
|
||||
function schemasFor(context: vscode.ExtensionContext): EditorSchemas {
|
||||
editorSchemas ??= new EditorSchemas(readSchemas(vscode.Uri.joinPath(context.extensionUri, 'schemas').fsPath));
|
||||
return editorSchemas;
|
||||
}
|
||||
|
||||
export function isSourceMessage(message: { type?: string }): message is SourceMessage {
|
||||
return message.type === 'sourceReady' || message.type === 'sourceOpen' || message.type === 'sourceClose' || message.type === 'sourceEdit';
|
||||
}
|
||||
|
||||
/**
|
||||
* The host half of a page's Source view. The document stays VS Code's: page
|
||||
* edits become WorkspaceEdits (so dirty state, save, undo and Git work as
|
||||
* for any file), and changes from anywhere else (undo, the text editor, git)
|
||||
* go back to the page. Problems are whatever the Problems panel holds for the
|
||||
* file, sent as offsets; the page shows their count on the Source switch even
|
||||
* while the picture is shown.
|
||||
*/
|
||||
export class SourceSession implements vscode.Disposable {
|
||||
/** True while the page shows Source; the view must not replace its HTML then. */
|
||||
shown = false;
|
||||
/** Texts our own edits will produce, oldest first; their change events are not echoed. */
|
||||
private readonly expected: string[] = [];
|
||||
private queue: Promise<void> = Promise.resolve();
|
||||
private schemaKey: string | undefined;
|
||||
private readonly disposables: vscode.Disposable[];
|
||||
|
||||
constructor(
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly panel: vscode.WebviewPanel,
|
||||
readonly uri: vscode.Uri,
|
||||
readonly kind: SourceKind,
|
||||
/** Called when Source opens or closes, so the view can hold or apply its HTML. */
|
||||
private readonly onShown: (shown: boolean) => void,
|
||||
) {
|
||||
const same = (uri: vscode.Uri) => uri.toString() === this.uri.toString();
|
||||
this.disposables = [
|
||||
vscode.languages.onDidChangeDiagnostics((event) => {
|
||||
if (event.uris.some(same)) void this.postDiagnostics();
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
if (!same(event.document.uri) || !event.contentChanges.length) return;
|
||||
this.post({ type: 'sourceDirty', dirty: event.document.isDirty });
|
||||
if (this.shown) this.syncFrom(event.document);
|
||||
}),
|
||||
vscode.workspace.onDidSaveTextDocument((doc) => {
|
||||
if (same(doc.uri)) this.post({ type: 'sourceDirty', dirty: false });
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/** Asks the page to show its Source view (Archify | Show Source). */
|
||||
reveal(): void {
|
||||
this.post({ type: 'setView', view: 'source' });
|
||||
}
|
||||
|
||||
/** Handles a Source message from the page; false for any other message. */
|
||||
handle(message: { type?: string }): boolean {
|
||||
if (!isSourceMessage(message)) return false;
|
||||
switch (message.type) {
|
||||
case 'sourceReady':
|
||||
void this.postDiagnostics();
|
||||
break;
|
||||
case 'sourceOpen':
|
||||
this.setShown(true);
|
||||
void this.sendText(true);
|
||||
break;
|
||||
case 'sourceClose':
|
||||
this.setShown(false);
|
||||
break;
|
||||
case 'sourceEdit':
|
||||
this.edit(message);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private setShown(shown: boolean): void {
|
||||
if (this.shown === shown) return;
|
||||
this.shown = shown;
|
||||
this.onShown(shown);
|
||||
}
|
||||
|
||||
private post(message: unknown): void {
|
||||
Promise.resolve(this.panel.webview.postMessage(message)).catch(() => undefined);
|
||||
}
|
||||
|
||||
private document(): Promise<vscode.TextDocument> {
|
||||
return Promise.resolve(vscode.workspace.openTextDocument(this.uri));
|
||||
}
|
||||
|
||||
private async sendText(withDiagnostics: boolean): Promise<void> {
|
||||
const doc = await this.document();
|
||||
const text = normalizeEol(doc.getText());
|
||||
this.expected.length = 0;
|
||||
this.schemaKey = undefined;
|
||||
this.post({ type: 'sourceText', text, schema: this.schemaChange(text), checks: CHECKS[this.kind] });
|
||||
this.post({ type: 'sourceDirty', dirty: doc.isDirty });
|
||||
if (withDiagnostics) await this.postDiagnostics();
|
||||
}
|
||||
|
||||
/** The editor schema when it differs from the one the page has (a diagram's type may change), else undefined. */
|
||||
private schemaChange(text: string): { key: string; schema: object } | null | undefined {
|
||||
const next = schemasFor(this.context).forSource(this.kind, text);
|
||||
const key = next?.key ?? '';
|
||||
if (key === this.schemaKey) return undefined;
|
||||
this.schemaKey = key;
|
||||
return next ?? null;
|
||||
}
|
||||
|
||||
private syncFrom(doc: vscode.TextDocument): void {
|
||||
const text = normalizeEol(doc.getText());
|
||||
const index = this.expected.indexOf(text);
|
||||
if (index !== -1) {
|
||||
// Our own edit coming back; anything older is settled too.
|
||||
this.expected.splice(0, index + 1);
|
||||
return;
|
||||
}
|
||||
this.post({ type: 'sourceText', text, schema: this.schemaChange(text), checks: CHECKS[this.kind] });
|
||||
}
|
||||
|
||||
private edit(message: Extract<SourceMessage, { type: 'sourceEdit' }>): void {
|
||||
this.expected.push(message.text);
|
||||
const schema = this.schemaChange(message.text);
|
||||
if (schema !== undefined) this.post({ type: 'sourceSchema', schema });
|
||||
this.queue = this.queue.then(async () => {
|
||||
try {
|
||||
const doc = await this.document();
|
||||
const current = normalizeEol(doc.getText());
|
||||
if (current === message.text) return;
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
// Positions come from "\n" text; VS Code turns inserted text into the document's line endings.
|
||||
const at = (text: string, offset: number) => {
|
||||
const { line, character } = positionOf(text, offset);
|
||||
return new vscode.Position(line, character);
|
||||
};
|
||||
if (current === message.base) {
|
||||
for (const change of message.changes) edit.replace(this.uri, new vscode.Range(at(current, change.from), at(current, change.to)), change.insert);
|
||||
} else {
|
||||
// The document moved on under the page (rare): the page's text wins.
|
||||
edit.replace(this.uri, new vscode.Range(at(current, 0), at(current, current.length)), message.text);
|
||||
}
|
||||
if (!(await vscode.workspace.applyEdit(edit))) {
|
||||
log().warn(`Source edit was not applied to ${this.uri.fsPath}`);
|
||||
void this.sendText(false);
|
||||
}
|
||||
} catch (error) {
|
||||
log().error(`Source edit failed for ${this.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Problems of the file as offsets into its "\n" text, and their count for the switch. */
|
||||
async postDiagnostics(): Promise<void> {
|
||||
const diagnostics = vscode.languages.getDiagnostics(this.uri);
|
||||
let items: unknown[] = [];
|
||||
if (this.shown && diagnostics.length) {
|
||||
const text = normalizeEol((await this.document()).getText());
|
||||
items = diagnostics.map((d) => ({
|
||||
from: offsetOf(text, d.range.start.line, d.range.start.character),
|
||||
to: offsetOf(text, d.range.end.line, d.range.end.character),
|
||||
severity: d.severity === vscode.DiagnosticSeverity.Error ? 'error' : d.severity === vscode.DiagnosticSeverity.Warning ? 'warning' : 'info',
|
||||
message: d.message,
|
||||
source: d.source,
|
||||
}));
|
||||
}
|
||||
const count = diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning).length;
|
||||
this.post({ type: 'sourceDiagnostics', items, count });
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables.splice(0)) d.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user