@ruckc/truss-code 0.0.0-stage → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CodeBlock.svelte +75 -0
- package/dist/CodeBlock.svelte.d.ts +17 -0
- package/dist/CodeEditor.svelte +227 -0
- package/dist/CodeEditor.svelte.d.ts +41 -0
- package/dist/DiffEditor.svelte +142 -0
- package/dist/DiffEditor.svelte.d.ts +32 -0
- package/dist/DiffFiles.svelte +196 -0
- package/dist/DiffFiles.svelte.d.ts +30 -0
- package/dist/DiffView.svelte +357 -0
- package/dist/DiffView.svelte.d.ts +56 -0
- package/dist/diff.d.ts +135 -0
- package/dist/diff.js +346 -0
- package/dist/editor-util.d.ts +46 -0
- package/dist/editor-util.js +52 -0
- package/dist/files.d.ts +44 -0
- package/dist/files.js +57 -0
- package/dist/highlight.d.ts +27 -0
- package/dist/highlight.js +84 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +12 -0
- package/dist/lang.d.ts +4 -0
- package/dist/lang.js +36 -0
- package/dist/monaco.d.ts +15 -0
- package/dist/monaco.js +40 -0
- package/dist/patch.d.ts +24 -0
- package/dist/patch.js +185 -0
- package/dist/theme.d.ts +80 -0
- package/dist/theme.js +217 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/dist/worker.d.ts +5 -0
- package/package.json +52 -5
- package/README.md +0 -3
package/dist/files.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { diffLines, diffStats } from './diff.js';
|
|
2
|
+
import { filePath, parsePatch } from './patch.js';
|
|
3
|
+
const STATUS = { added: 'added', modified: 'modified', deleted: 'deleted', renamed: 'renamed', copied: 'added' };
|
|
4
|
+
/** The files of a `git diff` as entries. */
|
|
5
|
+
export const entriesFromPatch = (patch) => parsePatch(patch).map((f) => ({
|
|
6
|
+
path: filePath(f),
|
|
7
|
+
oldPath: f.status === 'renamed' || f.status === 'copied' ? f.oldPath : undefined,
|
|
8
|
+
status: STATUS[f.status],
|
|
9
|
+
binary: f.binary,
|
|
10
|
+
additions: f.additions,
|
|
11
|
+
deletions: f.deletions,
|
|
12
|
+
file: f
|
|
13
|
+
}));
|
|
14
|
+
/** An input file as an entry, counting its changes with the same options the diff view will use. */
|
|
15
|
+
export function entryFromInput(f, opts = {}) {
|
|
16
|
+
const status = f.status ?? (f.oldPath && f.oldPath !== f.path ? 'renamed' : 'modified');
|
|
17
|
+
if (f.patch !== undefined) {
|
|
18
|
+
const parsed = parsePatch(f.patch)[0];
|
|
19
|
+
return { path: f.path, oldPath: f.oldPath, status, binary: !!(f.binary || parsed?.binary), additions: parsed?.additions ?? 0, deletions: parsed?.deletions ?? 0, file: parsed };
|
|
20
|
+
}
|
|
21
|
+
const binary = !!f.binary;
|
|
22
|
+
const s = binary ? { additions: 0, deletions: 0 } : diffStats(diffLines(f.old ?? '', f.new ?? '', opts));
|
|
23
|
+
return { path: f.path, oldPath: f.oldPath, status, binary, ...s, old: f.old ?? '', new: f.new ?? '' };
|
|
24
|
+
}
|
|
25
|
+
export const statusLetter = (s) => ({ added: 'A', modified: 'M', deleted: 'D', renamed: 'R' })[s];
|
|
26
|
+
export function totals(entries) {
|
|
27
|
+
return { files: entries.length, additions: entries.reduce((n, e) => n + e.additions, 0), deletions: entries.reduce((n, e) => n + e.deletions, 0) };
|
|
28
|
+
}
|
|
29
|
+
/** A folder tree of the changed files: folders first, then files, each sorted by name. Folder ids are `dir:` plus their path; file ids are the file's path. */
|
|
30
|
+
export function buildTree(entries) {
|
|
31
|
+
const top = { node: { id: '', label: '', children: [] }, dirs: new Map() };
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
const parts = e.path.split('/');
|
|
34
|
+
let at = top;
|
|
35
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
36
|
+
const id = 'dir:' + parts.slice(0, i + 1).join('/');
|
|
37
|
+
let d = at.dirs.get(id);
|
|
38
|
+
if (!d) {
|
|
39
|
+
d = { node: { id, label: parts[i], icon: 'folder', children: [] }, dirs: new Map() };
|
|
40
|
+
at.dirs.set(id, d);
|
|
41
|
+
at.node.children.push(d.node);
|
|
42
|
+
}
|
|
43
|
+
at = d;
|
|
44
|
+
}
|
|
45
|
+
at.node.children.push({ id: e.path, label: parts[parts.length - 1], icon: 'file', badge: statusLetter(e.status), description: `${e.status}, ${e.additions} added, ${e.deletions} removed` });
|
|
46
|
+
}
|
|
47
|
+
const sort = (list) => {
|
|
48
|
+
list.sort((a, b) => Number(!!b.children) - Number(!!a.children) || a.label.localeCompare(b.label));
|
|
49
|
+
for (const n of list)
|
|
50
|
+
if (n.children)
|
|
51
|
+
sort(n.children);
|
|
52
|
+
};
|
|
53
|
+
sort(top.node.children);
|
|
54
|
+
return top.node.children;
|
|
55
|
+
}
|
|
56
|
+
/** Ids of every folder in a tree, so the whole tree can start expanded. */
|
|
57
|
+
export const folderIds = (nodes) => nodes.flatMap((n) => (n.children ? [n.id, ...folderIds(n.children)] : []));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MonacoApi } from './monaco.js';
|
|
2
|
+
import { type Role, type Palette } from './theme.js';
|
|
3
|
+
/** A run of text with one syntax role (`undefined` is plain text). */
|
|
4
|
+
export interface Seg {
|
|
5
|
+
text: string;
|
|
6
|
+
role?: Role;
|
|
7
|
+
}
|
|
8
|
+
/** Splits one line at its token offsets and merges neighbours that share a role. */
|
|
9
|
+
export declare function segmentLine(line: string, tokens?: {
|
|
10
|
+
offset: number;
|
|
11
|
+
type: string;
|
|
12
|
+
}[]): Seg[];
|
|
13
|
+
/** The line is split on any line ending, exactly like Monaco's models do. */
|
|
14
|
+
export declare const splitLines: (text: string) => string[];
|
|
15
|
+
/** Tokenizes `text` with Monaco's tokenizers (loading the language on demand). Unknown languages and blank text come back as plain text. */
|
|
16
|
+
export declare function highlight(monaco: MonacoApi, text: string, language: string): Promise<Seg[][]>;
|
|
17
|
+
/** A run with a syntax role and, for diffs, whether an intra-line change covers it. */
|
|
18
|
+
export interface Piece extends Seg {
|
|
19
|
+
changed?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Lays intra-line change marks over syntax segments of the same line text, splitting runs where the two disagree. */
|
|
22
|
+
export declare function overlay(base: Seg[], marks?: {
|
|
23
|
+
text: string;
|
|
24
|
+
changed: boolean;
|
|
25
|
+
}[]): Piece[];
|
|
26
|
+
/** CSS custom properties that colour `.seg-<role>` text from a palette. Set them on the element that holds the code. */
|
|
27
|
+
export declare const paletteVars: (p: Palette) => string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { scopeRole } from './theme.js';
|
|
2
|
+
/** Splits one line at its token offsets and merges neighbours that share a role. */
|
|
3
|
+
export function segmentLine(line, tokens = []) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const push = (text, role) => {
|
|
6
|
+
if (!text)
|
|
7
|
+
return;
|
|
8
|
+
const last = out[out.length - 1];
|
|
9
|
+
if (last && last.role === role)
|
|
10
|
+
last.text += text;
|
|
11
|
+
else
|
|
12
|
+
out.push({ text, role });
|
|
13
|
+
};
|
|
14
|
+
if (!tokens.length) {
|
|
15
|
+
push(line);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
if (tokens[0].offset > 0)
|
|
19
|
+
push(line.slice(0, tokens[0].offset));
|
|
20
|
+
tokens.forEach((t, i) => push(line.slice(t.offset, tokens[i + 1]?.offset ?? line.length), scopeRole(t.type)));
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** The line is split on any line ending, exactly like Monaco's models do. */
|
|
24
|
+
export const splitLines = (text) => text.split(/\r\n|\r|\n/);
|
|
25
|
+
const typed = (t) => t.some((l) => l.some((x) => x.type));
|
|
26
|
+
const prepared = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* Waits until Monaco can tokenize `language`. Monarch languages are ready once `colorize` resolves; JSON and HTML only get their tokenizer
|
|
29
|
+
* after a model of that language exists, so one is made and the tokenizer awaited (at most about 1.6 s, once per language).
|
|
30
|
+
*/
|
|
31
|
+
function prepare(monaco, language, sample) {
|
|
32
|
+
let p = prepared.get(language);
|
|
33
|
+
if (!p) {
|
|
34
|
+
p = (async () => {
|
|
35
|
+
await monaco.editor.colorize('', language, {});
|
|
36
|
+
if (typed(monaco.editor.tokenize(sample, language)))
|
|
37
|
+
return;
|
|
38
|
+
const probe = monaco.editor.createModel('', language);
|
|
39
|
+
try {
|
|
40
|
+
for (let i = 0; i < 40 && !typed(monaco.editor.tokenize(sample, language)); i++)
|
|
41
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
probe.dispose();
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
prepared.set(language, p);
|
|
48
|
+
}
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
/** Tokenizes `text` with Monaco's tokenizers (loading the language on demand). Unknown languages and blank text come back as plain text. */
|
|
52
|
+
export async function highlight(monaco, text, language) {
|
|
53
|
+
const lines = splitLines(text);
|
|
54
|
+
if (language === 'plaintext' || !/\S/.test(text) || !monaco.languages.getLanguages().some((l) => l.id === language))
|
|
55
|
+
return lines.map((l) => segmentLine(l, []));
|
|
56
|
+
await prepare(monaco, language, text);
|
|
57
|
+
const tokens = monaco.editor.tokenize(text, language);
|
|
58
|
+
return lines.map((l, i) => segmentLine(l, tokens[i]));
|
|
59
|
+
}
|
|
60
|
+
/** Lays intra-line change marks over syntax segments of the same line text, splitting runs where the two disagree. */
|
|
61
|
+
export function overlay(base, marks) {
|
|
62
|
+
if (!marks)
|
|
63
|
+
return base;
|
|
64
|
+
const out = [];
|
|
65
|
+
let bi = 0, off = 0;
|
|
66
|
+
for (const m of marks) {
|
|
67
|
+
for (let left = m.text.length; left > 0;) {
|
|
68
|
+
const b = base[bi];
|
|
69
|
+
if (!b)
|
|
70
|
+
return out; // base shorter than the marks: nothing more to colour
|
|
71
|
+
const take = Math.min(left, b.text.length - off);
|
|
72
|
+
out.push({ text: b.text.slice(off, off + take), role: b.role, changed: m.changed || undefined });
|
|
73
|
+
off += take;
|
|
74
|
+
left -= take;
|
|
75
|
+
if (off === b.text.length) {
|
|
76
|
+
bi++;
|
|
77
|
+
off = 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** CSS custom properties that colour `.seg-<role>` text from a palette. Set them on the element that holds the code. */
|
|
84
|
+
export const paletteVars = (p) => Object.entries(p.roles).map(([r, c]) => `--code-${r}:${c}`).join(';') + `;--code-bg:${p.bg};--code-fg:${p.fg};--code-added:${p.added};--code-removed:${p.removed};--code-added-strong:${p.addedStrong};--code-removed-strong:${p.removedStrong}`;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { default as CodeEditor } from './CodeEditor.svelte';
|
|
2
|
+
export { default as CodeBlock } from './CodeBlock.svelte';
|
|
3
|
+
export { default as DiffView } from './DiffView.svelte';
|
|
4
|
+
export { default as DiffFiles } from './DiffFiles.svelte';
|
|
5
|
+
export { default as DiffEditor } from './DiffEditor.svelte';
|
|
6
|
+
export { detectLanguage, resolveLanguage } from './lang.js';
|
|
7
|
+
export { loadMonaco } from './monaco.js';
|
|
8
|
+
export { buildPalette, monacoTheme, ensureContrast, contrast, parseColor } from './theme.js';
|
|
9
|
+
export { diffLines, diffSeq, intraline, annotateIntraline, toHunks, formatHunks, unifiedPatch, foldItems, expandFold, splitRows, splitDocument, changeStarts, diffStats } from './diff.js';
|
|
10
|
+
export { parsePatch, patchLines, unquotePath, filePath } from './patch.js';
|
|
11
|
+
export { entriesFromPatch, buildTree } from './files.js';
|
|
12
|
+
export type { CodeMarker, MarkerSeverity, CompletionItem, CompletionKind, CompletionContext, DiffAnnotation } from './types.js';
|
|
13
|
+
export type { Palette, Role, Mode } from './theme.js';
|
|
14
|
+
export type { DiffLine, Hunk, DiffOptions, IntraSeg, Expansion } from './diff.js';
|
|
15
|
+
export type { PatchFile, FileStatus } from './patch.js';
|
|
16
|
+
export type { DiffFileInput, DiffStatus } from './files.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as CodeEditor } from './CodeEditor.svelte';
|
|
2
|
+
export { default as CodeBlock } from './CodeBlock.svelte';
|
|
3
|
+
export { default as DiffView } from './DiffView.svelte';
|
|
4
|
+
export { default as DiffFiles } from './DiffFiles.svelte';
|
|
5
|
+
export { default as DiffEditor } from './DiffEditor.svelte';
|
|
6
|
+
export { detectLanguage, resolveLanguage } from './lang.js';
|
|
7
|
+
export { loadMonaco } from './monaco.js';
|
|
8
|
+
export { buildPalette, monacoTheme, ensureContrast, contrast, parseColor } from './theme.js';
|
|
9
|
+
// pure diff and patch logic, usable without any component
|
|
10
|
+
export { diffLines, diffSeq, intraline, annotateIntraline, toHunks, formatHunks, unifiedPatch, foldItems, expandFold, splitRows, splitDocument, changeStarts, diffStats } from './diff.js';
|
|
11
|
+
export { parsePatch, patchLines, unquotePath, filePath } from './patch.js';
|
|
12
|
+
export { entriesFromPatch, buildTree } from './files.js';
|
package/dist/lang.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The Monaco language id for a file path, or `undefined` when the extension is unknown. */
|
|
2
|
+
export declare function detectLanguage(path?: string): string | undefined;
|
|
3
|
+
/** The language a component uses: an explicit one wins, then the path's, else plain text. */
|
|
4
|
+
export declare const resolveLanguage: (language?: string, path?: string) => string;
|
package/dist/lang.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Language ids are Monaco's. Extensions and a few well-known file names map to them. */
|
|
2
|
+
const EXT = {
|
|
3
|
+
ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript',
|
|
4
|
+
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
|
5
|
+
json: 'json', jsonc: 'json', json5: 'json', webmanifest: 'json',
|
|
6
|
+
yaml: 'yaml', yml: 'yaml',
|
|
7
|
+
md: 'markdown', markdown: 'markdown', mdx: 'markdown',
|
|
8
|
+
html: 'html', htm: 'html', svelte: 'html', vue: 'html',
|
|
9
|
+
css: 'css', scss: 'scss', less: 'less',
|
|
10
|
+
sql: 'sql', pgsql: 'pgsql', mysql: 'mysql',
|
|
11
|
+
py: 'python', pyw: 'python',
|
|
12
|
+
rs: 'rust', go: 'go', java: 'java', kt: 'kotlin', swift: 'swift', rb: 'ruby', php: 'php', lua: 'lua',
|
|
13
|
+
c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp', cs: 'csharp',
|
|
14
|
+
sh: 'shell', bash: 'shell', zsh: 'shell', ps1: 'powershell',
|
|
15
|
+
xml: 'xml', svg: 'xml', xsl: 'xml',
|
|
16
|
+
toml: 'ini', ini: 'ini', conf: 'ini', cfg: 'ini', properties: 'ini',
|
|
17
|
+
graphql: 'graphql', gql: 'graphql', proto: 'protobuf', tf: 'hcl', hcl: 'hcl',
|
|
18
|
+
dockerfile: 'dockerfile', bat: 'bat', cmd: 'bat', r: 'r', scala: 'scala', dart: 'dart', ex: 'elixir', exs: 'elixir',
|
|
19
|
+
txt: 'plaintext', log: 'plaintext', text: 'plaintext'
|
|
20
|
+
};
|
|
21
|
+
const NAMES = {
|
|
22
|
+
dockerfile: 'dockerfile', makefile: 'makefile', gemfile: 'ruby', rakefile: 'ruby', '.bashrc': 'shell', '.zshrc': 'shell', '.env': 'ini',
|
|
23
|
+
'.gitignore': 'plaintext', '.editorconfig': 'ini', '.babelrc': 'json', '.eslintrc': 'json'
|
|
24
|
+
};
|
|
25
|
+
/** The Monaco language id for a file path, or `undefined` when the extension is unknown. */
|
|
26
|
+
export function detectLanguage(path) {
|
|
27
|
+
if (!path)
|
|
28
|
+
return undefined;
|
|
29
|
+
const base = path.replace(/^.*[\\/]/, '').toLowerCase();
|
|
30
|
+
if (NAMES[base])
|
|
31
|
+
return NAMES[base];
|
|
32
|
+
const dot = base.lastIndexOf('.');
|
|
33
|
+
return dot < 0 ? undefined : EXT[base.slice(dot + 1)];
|
|
34
|
+
}
|
|
35
|
+
/** The language a component uses: an explicit one wins, then the path's, else plain text. */
|
|
36
|
+
export const resolveLanguage = (language, path) => language || detectLanguage(path) || 'plaintext';
|
package/dist/monaco.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as Monaco from 'monaco-editor';
|
|
2
|
+
import { type Theme } from './theme.js';
|
|
3
|
+
export type MonacoApi = typeof Monaco;
|
|
4
|
+
/** A worker constructor, as produced by Vite's `?worker` imports. */
|
|
5
|
+
export type WorkerFactory = () => Worker;
|
|
6
|
+
/** Monaco's worker label to the kind of worker that serves it. */
|
|
7
|
+
export declare const workerKind: (label: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Loads Monaco once (a separate chunk, never during SSR) and wires its workers through the bundler:
|
|
10
|
+
* every language service worker is its own lazy chunk that is fetched only when that language is used.
|
|
11
|
+
* If the app already set `self.MonacoEnvironment`, it is left alone.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadMonaco(): Promise<MonacoApi>;
|
|
14
|
+
/** Defines and selects the Truss theme for the tokens in effect at `host`. Monaco themes are global, so the last editor to call this wins. */
|
|
15
|
+
export declare function applyTheme(monaco: MonacoApi, host: Element): Theme;
|
package/dist/monaco.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readTheme, monacoTheme } from './theme.js';
|
|
2
|
+
let pending;
|
|
3
|
+
const workers = {
|
|
4
|
+
json: () => import('monaco-editor/language/json/json.worker.js?worker').then((m) => () => new m.default()),
|
|
5
|
+
css: () => import('monaco-editor/language/css/css.worker.js?worker').then((m) => () => new m.default()),
|
|
6
|
+
html: () => import('monaco-editor/language/html/html.worker.js?worker').then((m) => () => new m.default()),
|
|
7
|
+
typescript: () => import('monaco-editor/language/typescript/ts.worker.js?worker').then((m) => () => new m.default()),
|
|
8
|
+
editor: () => import('monaco-editor/editor/editor.worker.js?worker').then((m) => () => new m.default())
|
|
9
|
+
};
|
|
10
|
+
/** Monaco's worker label to the kind of worker that serves it. */
|
|
11
|
+
export const workerKind = (label) => label === 'json' ? 'json' : ['css', 'scss', 'less'].includes(label) ? 'css' : ['html', 'handlebars', 'razor'].includes(label) ? 'html' : ['typescript', 'javascript'].includes(label) ? 'typescript' : 'editor';
|
|
12
|
+
/**
|
|
13
|
+
* Loads Monaco once (a separate chunk, never during SSR) and wires its workers through the bundler:
|
|
14
|
+
* every language service worker is its own lazy chunk that is fetched only when that language is used.
|
|
15
|
+
* If the app already set `self.MonacoEnvironment`, it is left alone.
|
|
16
|
+
*/
|
|
17
|
+
export function loadMonaco() {
|
|
18
|
+
if (!pending) {
|
|
19
|
+
pending = (async () => {
|
|
20
|
+
const g = globalThis;
|
|
21
|
+
if (!g.MonacoEnvironment) {
|
|
22
|
+
const cache = new Map();
|
|
23
|
+
const ready = Promise.all(Object.entries(workers).map(async ([k, load]) => cache.set(k, await load())));
|
|
24
|
+
await ready;
|
|
25
|
+
g.MonacoEnvironment = { getWorker: (_id, label) => cache.get(workerKind(label))() };
|
|
26
|
+
}
|
|
27
|
+
return (await import('monaco-editor'));
|
|
28
|
+
})();
|
|
29
|
+
pending.catch(() => { pending = undefined; }); // a failed load (offline, blocked chunk) can be retried by the next mount
|
|
30
|
+
}
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
|
33
|
+
/** Defines and selects the Truss theme for the tokens in effect at `host`. Monaco themes are global, so the last editor to call this wins. */
|
|
34
|
+
export function applyTheme(monaco, host) {
|
|
35
|
+
const theme = readTheme(host);
|
|
36
|
+
const name = `truss-${theme.mode}`;
|
|
37
|
+
monaco.editor.defineTheme(name, monacoTheme(theme.palette));
|
|
38
|
+
monaco.editor.setTheme(name);
|
|
39
|
+
return theme;
|
|
40
|
+
}
|
package/dist/patch.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type DiffLine, type Hunk } from './diff.js';
|
|
2
|
+
export type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied';
|
|
3
|
+
/** One file of a patch. */
|
|
4
|
+
export interface PatchFile {
|
|
5
|
+
oldPath?: string;
|
|
6
|
+
newPath?: string;
|
|
7
|
+
status: FileStatus;
|
|
8
|
+
binary: boolean;
|
|
9
|
+
/** Percent, for renames and copies. */
|
|
10
|
+
similarity?: number;
|
|
11
|
+
oldMode?: string;
|
|
12
|
+
newMode?: string;
|
|
13
|
+
hunks: Hunk[];
|
|
14
|
+
additions: number;
|
|
15
|
+
deletions: number;
|
|
16
|
+
}
|
|
17
|
+
/** The path to show for a file: the new one, or the old one for a deletion. */
|
|
18
|
+
export declare const filePath: (f: Pick<PatchFile, "oldPath" | "newPath">) => string;
|
|
19
|
+
/** Decodes a path as `git diff` writes it: plain, or in double quotes with C escapes and UTF-8 bytes as octal (`\303\251`). */
|
|
20
|
+
export declare function unquotePath(raw: string): string;
|
|
21
|
+
/** Parses unified diff text, including `git diff` / `git format-patch` output with renames, mode changes, binary files, CRLF and "". */
|
|
22
|
+
export declare function parsePatch(text: string): PatchFile[];
|
|
23
|
+
/** The lines of a parsed file in display order, with a `gap` line for the unchanged lines a patch leaves out between (and before) hunks. */
|
|
24
|
+
export declare function patchLines(file: Pick<PatchFile, 'hunks'>): DiffLine[];
|
package/dist/patch.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { diffStats } from './diff.js';
|
|
2
|
+
/** The path to show for a file: the new one, or the old one for a deletion. */
|
|
3
|
+
export const filePath = (f) => f.newPath ?? f.oldPath ?? '';
|
|
4
|
+
const OCTAL = /\\([0-7]{3}|[abfnrtv"\\])/g;
|
|
5
|
+
const SIMPLE = { a: '\x07', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '"': '"', '\\': '\\' };
|
|
6
|
+
/** Decodes a path as `git diff` writes it: plain, or in double quotes with C escapes and UTF-8 bytes as octal (`\303\251`). */
|
|
7
|
+
export function unquotePath(raw) {
|
|
8
|
+
const s = raw.trim();
|
|
9
|
+
if (!(s.length >= 2 && s.startsWith('"') && s.endsWith('"')))
|
|
10
|
+
return s;
|
|
11
|
+
const bytes = [];
|
|
12
|
+
let last = 0;
|
|
13
|
+
const body = s.slice(1, -1);
|
|
14
|
+
const flush = (upto) => { for (const b of new TextEncoder().encode(body.slice(last, upto)))
|
|
15
|
+
bytes.push(b); };
|
|
16
|
+
for (const m of body.matchAll(OCTAL)) {
|
|
17
|
+
flush(m.index);
|
|
18
|
+
last = m.index + m[0].length;
|
|
19
|
+
bytes.push(m[1].length === 3 ? parseInt(m[1], 8) : SIMPLE[m[1]].charCodeAt(0));
|
|
20
|
+
}
|
|
21
|
+
flush(body.length);
|
|
22
|
+
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
23
|
+
}
|
|
24
|
+
/** A `---`/`+++` path: unquoted, minus the `a/` or `b/` prefix and a trailing tab-separated timestamp; `/dev/null` is `undefined`. */
|
|
25
|
+
function sidePath(raw, prefix) {
|
|
26
|
+
let p = raw.replace(/\t.*$/, '').trimEnd();
|
|
27
|
+
p = unquotePath(p);
|
|
28
|
+
if (p === '/dev/null')
|
|
29
|
+
return undefined;
|
|
30
|
+
return p.startsWith(prefix) ? p.slice(2) : p;
|
|
31
|
+
}
|
|
32
|
+
/** The two paths of a `diff --git a/x b/y` line (spaces and quoting included). The `---`/`+++` and rename lines that follow correct any doubt. */
|
|
33
|
+
function gitHeaderPaths(rest) {
|
|
34
|
+
const strip = (p, prefix) => (p.startsWith(prefix) ? p.slice(2) : p);
|
|
35
|
+
if (rest.includes('"')) {
|
|
36
|
+
const t = rest.match(/"(?:[^"\\]|\\.)*"|\S+/g); // a quote is a non-space, so there is always a match
|
|
37
|
+
return [strip(unquotePath(t[0] ?? ''), 'a/'), strip(unquotePath(t[1] ?? ''), 'b/')];
|
|
38
|
+
}
|
|
39
|
+
const len = (rest.length - 5) / 2; // `a/X b/X`: equal paths, the usual case, even with spaces in them
|
|
40
|
+
if (Number.isInteger(len) && len > 0 && rest.slice(2, 2 + len) === rest.slice(len + 5))
|
|
41
|
+
return [rest.slice(2, 2 + len), rest.slice(len + 5)];
|
|
42
|
+
const at = rest.indexOf(' b/');
|
|
43
|
+
return at < 0 ? [strip(rest, 'a/'), undefined] : [strip(rest.slice(0, at), 'a/'), rest.slice(at + 3)];
|
|
44
|
+
}
|
|
45
|
+
/** "" applies to the line just above it. */
|
|
46
|
+
const markNoEol = (h) => { const last = h.lines[h.lines.length - 1]; if (last)
|
|
47
|
+
last.noEol = true; };
|
|
48
|
+
const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
49
|
+
/** Parses unified diff text, including `git diff` / `git format-patch` output with renames, mode changes, binary files, CRLF and "". */
|
|
50
|
+
export function parsePatch(text) {
|
|
51
|
+
const rows = text.split('\n');
|
|
52
|
+
if (rows[rows.length - 1] === '')
|
|
53
|
+
rows.pop();
|
|
54
|
+
const files = [];
|
|
55
|
+
let f;
|
|
56
|
+
let git = false;
|
|
57
|
+
const start = () => {
|
|
58
|
+
const n = { status: 'modified', binary: false, hunks: [], additions: 0, deletions: 0 };
|
|
59
|
+
files.push(n);
|
|
60
|
+
return n;
|
|
61
|
+
};
|
|
62
|
+
const head = (s) => s.replace(/\r$/, '');
|
|
63
|
+
for (let i = 0; i < rows.length; i++) {
|
|
64
|
+
const row = head(rows[i]);
|
|
65
|
+
if (row.startsWith('diff --git ')) {
|
|
66
|
+
f = start();
|
|
67
|
+
git = true;
|
|
68
|
+
[f.oldPath, f.newPath] = gitHeaderPaths(row.slice(11));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!f && (row.startsWith('--- ') || row.startsWith('Binary files '))) {
|
|
72
|
+
f = start();
|
|
73
|
+
git = false;
|
|
74
|
+
}
|
|
75
|
+
if (!f)
|
|
76
|
+
continue; // text before the first file (a mail header, a commit message)
|
|
77
|
+
let m;
|
|
78
|
+
if (row.startsWith('--- ') && !f.hunks.length) {
|
|
79
|
+
f.oldPath = sidePath(row.slice(4), 'a/');
|
|
80
|
+
if (f.oldPath === undefined)
|
|
81
|
+
f.status = 'added';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (row.startsWith('+++ ') && !f.hunks.length) {
|
|
85
|
+
f.newPath = sidePath(row.slice(4), 'b/');
|
|
86
|
+
if (f.newPath === undefined)
|
|
87
|
+
f.status = 'deleted';
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (row.startsWith('--- ') && f.hunks.length && !git) {
|
|
91
|
+
f = start();
|
|
92
|
+
i--;
|
|
93
|
+
continue;
|
|
94
|
+
} // a second file in a plain unified diff
|
|
95
|
+
if ((m = /^new file mode (\d+)/.exec(row))) {
|
|
96
|
+
f.status = 'added';
|
|
97
|
+
f.newMode = m[1];
|
|
98
|
+
f.oldPath = undefined;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if ((m = /^deleted file mode (\d+)/.exec(row))) {
|
|
102
|
+
f.status = 'deleted';
|
|
103
|
+
f.oldMode = m[1];
|
|
104
|
+
f.newPath = undefined;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if ((m = /^old mode (\d+)/.exec(row))) {
|
|
108
|
+
f.oldMode = m[1];
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if ((m = /^new mode (\d+)/.exec(row))) {
|
|
112
|
+
f.newMode = m[1];
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if ((m = /^similarity index (\d+)%/.exec(row))) {
|
|
116
|
+
f.similarity = +m[1];
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if ((m = /^(rename|copy) from (.*)$/.exec(row))) {
|
|
120
|
+
f.status = m[1] === 'rename' ? 'renamed' : 'copied';
|
|
121
|
+
f.oldPath = unquotePath(m[2]);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if ((m = /^(rename|copy) to (.*)$/.exec(row))) {
|
|
125
|
+
f.newPath = unquotePath(m[2]);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (row.startsWith('Binary files ') || row === 'GIT binary patch') {
|
|
129
|
+
f.binary = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!(m = HUNK.exec(row)))
|
|
133
|
+
continue;
|
|
134
|
+
// a hunk: read exactly as many lines as its header promises, so content that looks like a header cannot end it early
|
|
135
|
+
const h = { oldStart: +m[1], oldLines: m[2] === undefined ? 1 : +m[2], newStart: +m[3], newLines: m[4] === undefined ? 1 : +m[4], section: m[5] || undefined, lines: [] };
|
|
136
|
+
let oldNo = h.oldLines ? h.oldStart : h.oldStart + 1, newNo = h.newLines ? h.newStart : h.newStart + 1;
|
|
137
|
+
let ro = h.oldLines, rn = h.newLines;
|
|
138
|
+
while ((ro > 0 || rn > 0) && i + 1 < rows.length) {
|
|
139
|
+
const raw = rows[++i];
|
|
140
|
+
const tag = raw[0] ?? ' ';
|
|
141
|
+
const body = raw.slice(1);
|
|
142
|
+
const cr = body.endsWith('\r');
|
|
143
|
+
const t = cr ? body.slice(0, -1) : body;
|
|
144
|
+
if (tag === '\\') {
|
|
145
|
+
markNoEol(h);
|
|
146
|
+
continue;
|
|
147
|
+
} // "\ No newline" in the middle of a hunk belongs to the line above it
|
|
148
|
+
if (tag === '-') {
|
|
149
|
+
h.lines.push({ type: 'del', oldNo: oldNo++, text: t, cr });
|
|
150
|
+
ro--;
|
|
151
|
+
}
|
|
152
|
+
else if (tag === '+') {
|
|
153
|
+
h.lines.push({ type: 'add', newNo: newNo++, text: t, cr });
|
|
154
|
+
rn--;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
h.lines.push({ type: 'ctx', oldNo: oldNo++, newNo: newNo++, text: t, cr });
|
|
158
|
+
ro--;
|
|
159
|
+
rn--;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
while (i + 1 < rows.length && rows[i + 1].startsWith('\\')) {
|
|
163
|
+
i++;
|
|
164
|
+
markNoEol(h);
|
|
165
|
+
}
|
|
166
|
+
f.hunks.push(h);
|
|
167
|
+
const s = diffStats(h.lines);
|
|
168
|
+
f.additions += s.additions;
|
|
169
|
+
f.deletions += s.deletions;
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
/** The lines of a parsed file in display order, with a `gap` line for the unchanged lines a patch leaves out between (and before) hunks. */
|
|
174
|
+
export function patchLines(file) {
|
|
175
|
+
const out = [];
|
|
176
|
+
let prevEnd = 0; // the last old line the previous hunk reached
|
|
177
|
+
for (const h of file.hunks) {
|
|
178
|
+
const first = h.oldLines ? h.oldStart : h.oldStart + 1;
|
|
179
|
+
if (first - 1 > prevEnd)
|
|
180
|
+
out.push({ type: 'gap', text: '', count: first - 1 - prevEnd });
|
|
181
|
+
out.push(...h.lines);
|
|
182
|
+
prevEnd = h.oldLines ? h.oldStart + h.oldLines - 1 : h.oldStart;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns the design tokens into an editor palette. Everything here is pure except `readTheme`, which reads the live tokens.
|
|
3
|
+
* The syntax colours come from the chart categorical palette (--trc-s1..8) and are nudged until they reach WCAG AA (4.5:1)
|
|
4
|
+
* on every background the text can sit on (editor, current line, selection), so any brand's tokens stay readable.
|
|
5
|
+
*/
|
|
6
|
+
export type RGBA = [number, number, number, number];
|
|
7
|
+
export type Mode = 'light' | 'dark';
|
|
8
|
+
export type Role = 'keyword' | 'string' | 'number' | 'type' | 'function' | 'variable' | 'constant' | 'operator' | 'tag' | 'attribute' | 'regexp' | 'comment' | 'invalid' | 'delimiter' | 'meta';
|
|
9
|
+
/** Raw custom-property values by name, e.g. `{ '--tr-surface': '#fbfbfe' }`. */
|
|
10
|
+
export type Tokens = Record<string, string | undefined>;
|
|
11
|
+
export declare const MIN_CONTRAST = 4.5;
|
|
12
|
+
/** Parses `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()` (comma or space syntax). Anything else is `null`. */
|
|
13
|
+
export declare function parseColor(input?: string | null): RGBA | null;
|
|
14
|
+
/** `#rrggbb`, or `#rrggbbaa` when the colour is translucent. */
|
|
15
|
+
export declare const toHex: ([r, g, b, a]: RGBA) => string;
|
|
16
|
+
/** Blends `a` toward `b` by `t` (0 = a, 1 = b); the result is opaque. */
|
|
17
|
+
export declare const mix: (a: RGBA, b: RGBA, t: number) => RGBA;
|
|
18
|
+
/** Paints a translucent colour over an opaque one. */
|
|
19
|
+
export declare const over: (fg: RGBA, bg: RGBA) => RGBA;
|
|
20
|
+
export declare function luminance([r, g, b]: RGBA): number;
|
|
21
|
+
export declare function contrast(a: RGBA, b: RGBA): number;
|
|
22
|
+
/** The colour closest to `fg` (blended toward black or white) that reaches `min`:1 on every background. */
|
|
23
|
+
export declare function ensureContrast(fg: RGBA, bgs: RGBA[], min?: number): RGBA;
|
|
24
|
+
export declare const TOKEN_VARS: readonly ["--tr-surface", "--tr-surface-container-low", "--tr-surface-container", "--tr-surface-container-high", "--tr-on-surface", "--tr-on-surface-variant", "--tr-outline", "--tr-outline-variant", "--tr-primary", "--tr-danger", "--tr-warning", "--tr-success", "--trc-s1", "--trc-s2", "--trc-s3", "--trc-s4", "--trc-s5", "--trc-s6", "--trc-s7", "--trc-s8", "--tr-syntax-keyword", "--tr-syntax-string", "--tr-syntax-number", "--tr-syntax-comment", "--tr-syntax-function", "--tr-syntax-type", "--tr-syntax-variable", "--tr-syntax-constant", "--tr-syntax-operator", "--tr-syntax-punctuation", "--tr-syntax-tag", "--tr-syntax-attr", "--tr-syntax-regexp"];
|
|
25
|
+
/** Built-in values, used for any token that is not defined (the charts stylesheet is optional). They match tokens.css and charts.css. */
|
|
26
|
+
export declare const FALLBACK: Record<Mode, Record<string, string>>;
|
|
27
|
+
export declare const ROLES: Role[];
|
|
28
|
+
export interface Palette {
|
|
29
|
+
mode: Mode;
|
|
30
|
+
bg: string;
|
|
31
|
+
fg: string;
|
|
32
|
+
lineHighlight: string;
|
|
33
|
+
selection: string;
|
|
34
|
+
cursor: string;
|
|
35
|
+
muted: string;
|
|
36
|
+
border: string;
|
|
37
|
+
widget: string;
|
|
38
|
+
/** Diff backgrounds: a line, and the stronger mark for the changed words inside it. */
|
|
39
|
+
added: string;
|
|
40
|
+
removed: string;
|
|
41
|
+
addedStrong: string;
|
|
42
|
+
removedStrong: string;
|
|
43
|
+
roles: Record<Role, string>;
|
|
44
|
+
/** the lowest contrast ratio of any syntax colour on any editor background, for tests and the docs */
|
|
45
|
+
minContrast: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function buildPalette(tokens: Tokens, mode: Mode): Palette;
|
|
48
|
+
/** Monaco token types (and their dotted prefixes) to a role. The longest matching prefix wins, so `string.key.json` beats `string`. */
|
|
49
|
+
export declare const SCOPES: [prefix: string, role: Role, style?: 'italic' | 'bold'][];
|
|
50
|
+
/** The role of a Monaco token type such as `keyword.sql`, or `undefined` for plain text. */
|
|
51
|
+
export declare function scopeRole(type: string): Role | undefined;
|
|
52
|
+
export interface ThemeData {
|
|
53
|
+
base: 'vs' | 'vs-dark';
|
|
54
|
+
inherit: boolean;
|
|
55
|
+
rules: {
|
|
56
|
+
token: string;
|
|
57
|
+
foreground?: string;
|
|
58
|
+
background?: string;
|
|
59
|
+
fontStyle?: string;
|
|
60
|
+
}[];
|
|
61
|
+
colors: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
/** The Monaco theme for a palette. */
|
|
64
|
+
export declare function monacoTheme(p: Palette): ThemeData;
|
|
65
|
+
export interface Metrics {
|
|
66
|
+
fontSize: number;
|
|
67
|
+
lineHeight: number;
|
|
68
|
+
fontFamily: string;
|
|
69
|
+
}
|
|
70
|
+
/** Editor type metrics from the density-scaled tokens: `--tr-text-md` (px) and `--tr-leading-normal` (a ratio). */
|
|
71
|
+
export declare function fontMetrics(textMd?: string, leading?: string, family?: string): Metrics;
|
|
72
|
+
export interface Theme {
|
|
73
|
+
mode: Mode;
|
|
74
|
+
palette: Palette;
|
|
75
|
+
metrics: Metrics;
|
|
76
|
+
}
|
|
77
|
+
/** The theme in effect at `el`: nearest `data-theme`, else the OS preference; tokens and density as computed there. */
|
|
78
|
+
export declare function readTheme(el: Element): Theme;
|
|
79
|
+
/** Calls `cb` whenever the theme or density can have changed (`data-theme`, `data-density`, class or style on any ancestor, or the OS scheme). Returns a stop function. */
|
|
80
|
+
export declare function watchTheme(el: Element, cb: () => void): () => void;
|