дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
perf(templates): load Emmet when an HTML or stylesheet editor opens
The Emmet engine (~70 KB minified) was part of the initial bundle; it now loads with the first editor that can expand abbreviations, or with Update Tag with Emmet.
Цей коміт міститься в:
1 джерело
b91025a48e
коміт
4ceae47d1f
4 files changed
+35
-7
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { enclosingTag, expandAt, syntaxAt, updateTagEdits, type TextEdit } from "../emmet/model";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { enclosingTag, expandAt, loadEmmet, syntaxAt, updateTagEdits, type TextEdit } from "../emmet/model";
|
||||
|
||||
beforeAll(() => loadEmmet());
|
||||
|
||||
const apply = (text: string, edits: TextEdit[]) =>
|
||||
[...edits].sort((a, b) => b.from - a.from).reduce((t, e) => t.slice(0, e.from) + e.insert + t.slice(e.to), text);
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { usePostfixSettings } from "./postfix/store";
|
||||
import { showChooser, type ChooserItem } from "./popup/store";
|
||||
import { expandEmmetAtCaret } from "./emmet/editor";
|
||||
import { hasEmmet, loadEmmet } from "./emmet/model";
|
||||
|
||||
export interface TemplateDocInfo {
|
||||
path: string;
|
||||
@@ -557,6 +558,7 @@ export const templateCompletionSource: CompletionSource = (ctx: CompletionContex
|
||||
// Extension
|
||||
|
||||
export function templateEditorExtension(ctx: EditorDocContext): Extension {
|
||||
if (hasEmmet(ctx.language)) void loadEmmet().catch(() => {});
|
||||
return [
|
||||
templateDoc.of({ path: ctx.path, language: ctx.language }),
|
||||
fieldCompletion,
|
||||
|
||||
@@ -4,7 +4,22 @@
|
||||
* with Emmet" on the enclosing tag. Pure functions over document text; the
|
||||
* editor glue lives in `./editor`.
|
||||
*/
|
||||
import expand, { extract, type UserConfig } from "emmet";
|
||||
import type { UserConfig } from "emmet";
|
||||
|
||||
type Emmet = typeof import("emmet");
|
||||
let emmet: Emmet | null = null;
|
||||
let loading: Promise<Emmet> | null = null;
|
||||
|
||||
/** Loads the Emmet engine, kept out of start-up; expansion finds nothing until it has. */
|
||||
export function loadEmmet(): Promise<Emmet> {
|
||||
return (loading ??= import("emmet").then(
|
||||
(m) => (emmet = m),
|
||||
(e: unknown) => {
|
||||
loading = null;
|
||||
throw e;
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
export interface EmmetSyntax {
|
||||
type: "markup" | "stylesheet";
|
||||
@@ -15,6 +30,9 @@ export interface EmmetSyntax {
|
||||
const MARKUP_LANGUAGES = new Set(["html", "vue", "svelte"]);
|
||||
const STYLESHEET_LANGUAGES: Record<string, string> = { css: "css", scss: "scss", less: "less", sass: "sass" };
|
||||
|
||||
/** Whether `language` files expand Emmet abbreviations. */
|
||||
export const hasEmmet = (language: string) => MARKUP_LANGUAGES.has(language) || language in STYLESHEET_LANGUAGES;
|
||||
|
||||
/** Text before the caret that is scanned for context. */
|
||||
const CONTEXT_LIMIT = 200_000;
|
||||
|
||||
@@ -96,14 +114,18 @@ export interface Expansion {
|
||||
|
||||
/** The abbreviation ending at `col` of `line` expanded, or null when there is none. */
|
||||
export function expandAt(line: string, col: number, syntax: EmmetSyntax): Expansion | null {
|
||||
const found = extract(line, col, { type: syntax.type, lookAhead: true });
|
||||
if (!emmet) {
|
||||
void loadEmmet().catch(() => {});
|
||||
return null;
|
||||
}
|
||||
const found = emmet.extract(line, col, { type: syntax.type, lookAhead: true });
|
||||
if (!found || !found.abbreviation.trim()) return null;
|
||||
const abbr = found.abbreviation;
|
||||
// A bare number or a closing-tag fragment is never meant as an abbreviation.
|
||||
if (/^\d/.test(abbr) || abbr.startsWith("/")) return null;
|
||||
let snippet: string;
|
||||
try {
|
||||
snippet = expand(abbr, config(syntax, true));
|
||||
snippet = emmet.default(abbr, config(syntax, true));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -194,9 +216,10 @@ export interface TextEdit {
|
||||
export function updateTagEdits(tag: TagRange, abbreviation: string): TextEdit[] | null {
|
||||
const abbr = abbreviation.trim();
|
||||
if (!abbr || /[>+^*{]/.test(abbr.replace(/\[[^\]]*\]/g, ""))) return null;
|
||||
if (!emmet) return null;
|
||||
let html: string;
|
||||
try {
|
||||
html = expand(abbr, config({ type: "markup", syntax: "html" }, false));
|
||||
html = emmet.default(abbr, config({ type: "markup", syntax: "html" }, false));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { fileTemplates, useFileTemplates, watchTemplateProject } from "./files/s
|
||||
import type { FileTemplate } from "./files/model";
|
||||
import { syncTemplatesFromStorage } from "./sync";
|
||||
import { tagAtCaret, updateTag } from "./emmet/editor";
|
||||
import { loadEmmet } from "./emmet/model";
|
||||
import { promptAbbreviation, UpdateTagDialogHost } from "./emmet/UpdateTagDialog";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
@@ -35,7 +36,7 @@ async function updateTagWithEmmet() {
|
||||
notifications().notify({ level: "info", title: "No tag at the caret", key: "emmet-update-tag" });
|
||||
return;
|
||||
}
|
||||
const abbreviation = await promptAbbreviation(tag);
|
||||
const [abbreviation] = await Promise.all([promptAbbreviation(tag), loadEmmet()]);
|
||||
editor.view.focus();
|
||||
if (abbreviation && !updateTag(editor.view, abbreviation))
|
||||
notifications().notify({
|
||||
|
||||
Reference in new issue
Заблокувати користувача