@kubohiroya/turbowarp-plus-editor-project-model 0.1.1 → 0.2.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/README.ja.md CHANGED
@@ -10,7 +10,10 @@ TurboWarp Plus Editor で、実行中の Scratch 作品を画面なしで読み
10
10
  | --- | --- |
11
11
  | `@kubohiroya/turbowarp-plus-editor-project-model/project-messages` | どのブロックが各メッセージを送り、受け取るか(`scanProjectMessages`・`scanTargetMessages`・`projectMessageGraph`・`messageReference`)と、メッセージの名前を変えられるか(`checkMessageRename`) |
12
12
  | `@kubohiroya/turbowarp-plus-editor-project-model/sprite-variable-edit` | 編集中のスプライトまたはステージの変数の作成・名前変更・削除(`editSpriteVariable`)。重複する名前を拒み、クラウド変数の接頭辞を保ち、ブロックが使っている変数は削除しない |
13
- | `@kubohiroya/turbowarp-plus-editor-project-model` | 上の両方 |
13
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search` | 作品全体の検索(`searchProject`・`searchYamlDocuments`・`countVariableAccesses`)。ブロック・変数・リスト・メッセージ・コメント・コスチューム・音を探し、変数ごとに読み書きの箇所を数える |
14
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search-replace` | 検索結果の置換(`applyProjectSearchReplacement`・`renameProjectMessage`・`replacementText`)。Blockly とワークスペースは構造的な型で受け取り、古くなった結果・不正な名前・重複を拒む |
15
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search-connections` | 検索と Editor のつなぎ(作品ごとの移動先・YAML 文書・置換を開く口、置換のショートカット) |
16
+ | `@kubohiroya/turbowarp-plus-editor-project-model` | 上のすべて |
14
17
 
15
18
  ## 検証
16
19
 
@@ -18,4 +21,6 @@ TurboWarp Plus Editor で、実行中の Scratch 作品を画面なしで読み
18
21
 
19
22
  ## ライセンス
20
23
 
21
- MPL-2.0。`@kubohiroya/turbowarp-plus-editor-layout` から切り出したコードで、その履歴をこのリポジトリに残している。
24
+ MPL-2.0。`project-messages`・`sprite-variable-edit` は `@kubohiroya/turbowarp-plus-editor-layout` から切り出したコードで、その履歴をこのリポジトリに残している。
25
+
26
+ `project-search`・`project-search-replace`・`project-search-connections` は、turbowarp-plus-editor の `packages/editor-ui` から移した。同じ作者が書いたもので、GPL-3.0 から MPL-2.0 にした。由来の監査は [turbowarp-plus-packages の移行計画](https://github.com/kubohiroya/turbowarp-plus-packages/blob/main/docs/editor-migration-plan.ja.md)の「由来の監査(7b)」にある。
package/README.md CHANGED
@@ -10,7 +10,10 @@ Reads and edits a running Scratch project for TurboWarp Plus Editor, without any
10
10
  | --- | --- |
11
11
  | `@kubohiroya/turbowarp-plus-editor-project-model/project-messages` | Which blocks send or receive each broadcast message (`scanProjectMessages`, `scanTargetMessages`, `projectMessageGraph`, `messageReference`), and whether a message can be renamed (`checkMessageRename`) |
12
12
  | `@kubohiroya/turbowarp-plus-editor-project-model/sprite-variable-edit` | Creating, renaming and deleting a variable of the editing sprite or the stage (`editSpriteVariable`). It refuses duplicate names, keeps the cloud prefix, and refuses to delete a variable that a block uses |
13
- | `@kubohiroya/turbowarp-plus-editor-project-model` | Both of the above |
13
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search` | Search across the whole project (`searchProject`, `searchYamlDocuments`, `countVariableAccesses`): blocks, variables, lists, messages, comments, costumes and sounds, with the places each variable is read and written |
14
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search-replace` | Replace a search result (`applyProjectSearchReplacement`, `renameProjectMessage`, `replacementText`). Blockly and the workspace come in as structural types; stale results, invalid names and duplicates are refused |
15
+ | `@kubohiroya/turbowarp-plus-editor-project-model/project-search-connections` | What joins the search to the Editor: per-project hooks for navigation, YAML documents and opening replace, and the replace shortcut |
16
+ | `@kubohiroya/turbowarp-plus-editor-project-model` | All of the above |
14
17
 
15
18
  ## Verification
16
19
 
@@ -18,4 +21,6 @@ Run `npm test` to build the package and run the unit tests.
18
21
 
19
22
  ## License
20
23
 
21
- MPL-2.0. This code was split out of `@kubohiroya/turbowarp-plus-editor-layout`, and its history is kept in this repository.
24
+ MPL-2.0. `project-messages` and `sprite-variable-edit` were split out of `@kubohiroya/turbowarp-plus-editor-layout`, and their history is kept in this repository.
25
+
26
+ `project-search`, `project-search-replace` and `project-search-connections` moved here from `packages/editor-ui` in turbowarp-plus-editor. The same author wrote them, and they changed from GPL-3.0 to MPL-2.0 in the move. The provenance audit is in the "由来の監査(7b)" section of the [migration plan in turbowarp-plus-packages](https://github.com/kubohiroya/turbowarp-plus-packages/blob/main/docs/editor-migration-plan.ja.md).
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from './project-messages.ts';
2
2
  export * from './sprite-variable-edit.ts';
3
+ export * from './project-search.ts';
4
+ export * from './project-search-replace.ts';
5
+ export * from './project-search-connections.ts';
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from "./project-messages.js";
2
2
  export * from "./sprite-variable-edit.js";
3
+ export * from "./project-search.js";
4
+ export * from "./project-search-replace.js";
5
+ export * from "./project-search-connections.js";
@@ -0,0 +1,62 @@
1
+ import type { ProjectSearchResult, SearchProject, VariableSearch, YamlSearchDocument, YamlSearchResult } from './project-search.ts';
2
+ import type { ReplaceBlockly, ReplaceWorkspace } from './project-search-replace.ts';
3
+ export type SearchDocuments = {
4
+ getSearchableYamlDocuments(): readonly YamlSearchDocument[];
5
+ openYamlPreview(reference: YamlSearchDocument['reference'], range: YamlSearchResult['range']): boolean;
6
+ stageYamlReplacement?(reference: YamlSearchDocument['reference'], range: YamlSearchResult['range'], source: string, text: string): boolean;
7
+ };
8
+ type SearchView = {
9
+ id: string;
10
+ groupId: string;
11
+ targetId: string;
12
+ section: number;
13
+ };
14
+ type SearchViews = {
15
+ views(): SearchView[];
16
+ currentView(): SearchView | undefined;
17
+ activateView(id: string): boolean;
18
+ select(id: string): void;
19
+ setViewSection(id: string, section: number): boolean;
20
+ selectAsset(id: string, kind: 'costumes' | 'sounds', index: number): void;
21
+ };
22
+ type Target = ProjectSearchResult['target'];
23
+ export type SearchNavigation = {
24
+ getViews(): SearchViews | null;
25
+ getTarget(id: string): Target | undefined;
26
+ getEditingTarget(): Target | undefined;
27
+ centerBlock(id: string): void;
28
+ centerComment(id: string, blockId?: string | null): void;
29
+ replacementContext?(): {
30
+ workspace: ReplaceWorkspace;
31
+ Blockly: ReplaceBlockly;
32
+ } | undefined;
33
+ };
34
+ type SearchConnection = {
35
+ navigation?: SearchNavigation;
36
+ documents?: SearchDocuments;
37
+ openReplace?: () => void;
38
+ openVariable?: (filter: VariableSearch) => void;
39
+ listeners: Set<() => void>;
40
+ };
41
+ export declare function connectionFor(project: SearchProject): SearchConnection;
42
+ /** Open a target's view in the active group and switch it to a section. */
43
+ export declare function activateTargetView(navigation: SearchNavigation, targetId: string, section: number): {
44
+ views: SearchViews;
45
+ viewId: string;
46
+ section: number;
47
+ } | undefined;
48
+ export declare function connectProjectSearchNavigation(project: SearchProject, navigation: SearchNavigation): () => void;
49
+ export declare function connectProjectSearchDocuments(project: SearchProject, documents: SearchDocuments): () => void;
50
+ export declare function connectProjectReplaceOpener(project: SearchProject, open: () => void): () => void;
51
+ export declare function projectReplaceOpener(project?: SearchProject): (() => void) | undefined;
52
+ export declare function connectVariableSearchOpener(project: SearchProject, open: (filter: VariableSearch) => void): () => void;
53
+ export declare function openVariableSearch(project: SearchProject, filter: VariableSearch): void;
54
+ export declare function isMacPlatform(): boolean;
55
+ export declare const projectReplaceShortcut: {
56
+ keys: string;
57
+ macKeys: string;
58
+ label: string;
59
+ macLabel: string;
60
+ matches(event: KeyboardEvent): boolean;
61
+ };
62
+ export {};
@@ -0,0 +1,87 @@
1
+ const connections = new WeakMap();
2
+ export function connectionFor(project) {
3
+ let connection = connections.get(project);
4
+ if (!connection) {
5
+ connection = { listeners: new Set() };
6
+ connections.set(project, connection);
7
+ }
8
+ return connection;
9
+ }
10
+ /** Open a target's view in the active group and switch it to a section. */
11
+ export function activateTargetView(navigation, targetId, section) {
12
+ const views = navigation.getViews();
13
+ if (!views)
14
+ return undefined;
15
+ const groupId = views.currentView()?.groupId;
16
+ const local = views.views().find(view => view.groupId === groupId && view.targetId === targetId);
17
+ if (local)
18
+ views.activateView(local.id);
19
+ else
20
+ views.select(targetId);
21
+ const destination = views.currentView();
22
+ if (destination?.targetId !== targetId)
23
+ return undefined;
24
+ views.setViewSection(destination.id, section);
25
+ return { views, viewId: destination.id, section };
26
+ }
27
+ export function connectProjectSearchNavigation(project, navigation) {
28
+ const connection = connectionFor(project);
29
+ connection.navigation = navigation;
30
+ connection.listeners.forEach(listener => listener());
31
+ return () => {
32
+ if (connection.navigation !== navigation)
33
+ return;
34
+ connection.navigation = undefined;
35
+ connection.listeners.forEach(listener => listener());
36
+ };
37
+ }
38
+ export function connectProjectSearchDocuments(project, documents) {
39
+ const connection = connectionFor(project);
40
+ connection.documents = documents;
41
+ connection.listeners.forEach(listener => listener());
42
+ return () => {
43
+ if (connection.documents !== documents)
44
+ return;
45
+ connection.documents = undefined;
46
+ connection.listeners.forEach(listener => listener());
47
+ };
48
+ }
49
+ export function connectProjectReplaceOpener(project, open) {
50
+ const connection = connectionFor(project);
51
+ connection.openReplace = open;
52
+ return () => {
53
+ if (connection.openReplace === open)
54
+ connection.openReplace = undefined;
55
+ };
56
+ }
57
+ export function projectReplaceOpener(project) {
58
+ return project && connections.get(project)?.openReplace;
59
+ }
60
+ export function connectVariableSearchOpener(project, open) {
61
+ const connection = connectionFor(project);
62
+ connection.openVariable = open;
63
+ return () => {
64
+ if (connection.openVariable === open)
65
+ connection.openVariable = undefined;
66
+ };
67
+ }
68
+ export function openVariableSearch(project, filter) {
69
+ connections.get(project)?.openVariable?.(filter);
70
+ }
71
+ const replaceKey = 'F';
72
+ export function isMacPlatform() {
73
+ return typeof navigator !== 'undefined' && /^(Mac|iP)/.test(navigator.platform ?? '');
74
+ }
75
+ // Search is the platform's own find shortcut, so replace is the shifted form of it: ⇧⌘F on macOS
76
+ // and Ctrl+Shift+F elsewhere.
77
+ export const projectReplaceShortcut = {
78
+ keys: `Control+Shift+${replaceKey}`,
79
+ macKeys: `Meta+Shift+${replaceKey}`,
80
+ label: `Ctrl+Shift+${replaceKey}`,
81
+ macLabel: `⇧⌘${replaceKey}`,
82
+ matches(event) {
83
+ const primary = isMacPlatform() ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
84
+ return primary && event.shiftKey && !event.altKey && !event.isComposing &&
85
+ event.key.toUpperCase() === replaceKey;
86
+ }
87
+ };
@@ -0,0 +1,50 @@
1
+ import { type ProjectSearchResult, type SearchProject, type SearchResult } from './project-search.ts';
2
+ type Field = {
3
+ getValue(): unknown;
4
+ setValue(value: string): void;
5
+ callValidator(value: string): unknown;
6
+ getOptions?(): ReadonlyArray<readonly [unknown, unknown]>;
7
+ };
8
+ type Comment = {
9
+ getText(): string;
10
+ setText(value: string): void;
11
+ };
12
+ type Block = {
13
+ isEditable(): boolean;
14
+ getField(name: string): Field | undefined;
15
+ comment?: Comment;
16
+ mutationToDom(): Element;
17
+ };
18
+ type RenameEvent = {
19
+ type: string;
20
+ varId?: string;
21
+ newName?: string;
22
+ };
23
+ export type ReplaceWorkspace = {
24
+ options: {
25
+ readOnly: boolean;
26
+ };
27
+ isDragging(): boolean;
28
+ getBlockById(id: string): Block | null;
29
+ getCommentById(id: string): Comment | null;
30
+ getVariableById(id: string): {
31
+ name: string;
32
+ } | null;
33
+ renameVariableById(id: string, name: string): void;
34
+ addChangeListener(listener: (event: RenameEvent) => void): void;
35
+ };
36
+ export type ReplaceBlockly = {
37
+ FieldTextInput: abstract new (...args: never[]) => Field;
38
+ FieldDropdown: abstract new (...args: never[]) => Field;
39
+ FieldNumber: abstract new (...args: never[]) => Field;
40
+ Procedures: {
41
+ getPrototypeBlock(name: string, workspace: ReplaceWorkspace): Block | null;
42
+ mutateCallersAndPrototype(name: string, workspace: ReplaceWorkspace, mutation: Element): void;
43
+ };
44
+ };
45
+ export type ReplaceOutcome = 'applied' | 'staged' | 'stale' | 'conflict' | 'invalid' | 'read-only';
46
+ export declare function replacementText(result: SearchResult, query: string, replacement: string): string | undefined;
47
+ /** Rename a message from the message section, where no block result carries the edit. */
48
+ export declare function renameProjectMessage(project: SearchProject, workspace: ReplaceWorkspace, id: string, next: string): ReplaceOutcome;
49
+ export declare function applyProjectSearchReplacement(project: SearchProject, workspace: ReplaceWorkspace, Blockly: ReplaceBlockly, result: ProjectSearchResult, next: string): ReplaceOutcome;
50
+ export {};
@@ -0,0 +1,183 @@
1
+ import { literalSearchPattern } from "./project-search.js";
2
+ function spliceMatch(value, pattern, replacement) {
3
+ const match = pattern.exec(value);
4
+ if (match)
5
+ return value.slice(0, match.index) + replacement + value.slice(match.index + match[0].length);
6
+ }
7
+ function replaceProcedureLabel(name, pattern, replacement) {
8
+ if (/%[sbn]/.test(replacement))
9
+ return;
10
+ const parts = name.split(/(%[sbn])/);
11
+ for (let i = 0; i < parts.length; i += 2) {
12
+ const changed = spliceMatch(parts[i], pattern, replacement);
13
+ if (changed === undefined)
14
+ continue;
15
+ parts[i] = changed;
16
+ const next = parts.join('');
17
+ if (JSON.stringify(next.match(/%[sbn]/g)) === JSON.stringify(name.match(/%[sbn]/g)))
18
+ return next;
19
+ return;
20
+ }
21
+ }
22
+ export function replacementText(result, query, replacement) {
23
+ if (!query.trim())
24
+ return;
25
+ if (result.kind === 'yaml')
26
+ return replacement !== result.range.expectedText ? replacement : undefined;
27
+ const edit = result.edit;
28
+ if (!edit)
29
+ return;
30
+ const pattern = literalSearchPattern(query, 'iu');
31
+ const source = edit.type === 'name' || edit.type === 'procedure' ? edit.name : edit.type === 'comment' ? edit.text : edit.value;
32
+ const next = edit.type === 'procedure' ? replaceProcedureLabel(source, pattern, replacement) :
33
+ spliceMatch(source, pattern, replacement);
34
+ return next !== source ? next : undefined;
35
+ }
36
+ const watchedBroadcasts = new WeakMap();
37
+ function maintainBroadcastReferences(project, workspace, id) {
38
+ let ids = watchedBroadcasts.get(workspace);
39
+ if (!ids) {
40
+ const watched = new Set();
41
+ watchedBroadcasts.set(workspace, watched);
42
+ ids = watched;
43
+ workspace.addChangeListener(event => {
44
+ if (event.type !== 'var_rename' || !event.varId || !watched.has(event.varId))
45
+ return;
46
+ const variable = project.targets.find(target => target.isStage)?.variables[event.varId];
47
+ if (variable?.type !== 'broadcast_msg' || variable.name !== event.newName || variable.value === variable.name)
48
+ return;
49
+ variable.value = variable.name;
50
+ for (const blocks of new Set(project.targets.map(target => target.blocks))) {
51
+ const pending = [...blocks.getScripts()];
52
+ const seen = new Set();
53
+ while (pending.length) {
54
+ const id = pending.pop();
55
+ if (!id || seen.has(id))
56
+ continue;
57
+ seen.add(id);
58
+ const block = blocks.getBlock(id);
59
+ if (!block)
60
+ continue;
61
+ const field = block.fields?.BROADCAST_OPTION;
62
+ if (field?.id === event.varId)
63
+ field.value = variable.name;
64
+ if (block.next)
65
+ pending.push(block.next);
66
+ for (const input of Object.values(block.inputs ?? {})) {
67
+ if (input.block)
68
+ pending.push(input.block);
69
+ if (input.shadow)
70
+ pending.push(input.shadow);
71
+ }
72
+ }
73
+ }
74
+ project.resetAllCaches?.();
75
+ project.emitProjectChanged?.();
76
+ });
77
+ }
78
+ ids.add(id);
79
+ }
80
+ function validName(value) {
81
+ return Boolean(value.trim()) && value === value.trim() && !/[\u0000-\u001f\u007f]/u.test(value);
82
+ }
83
+ /** Rename a message from the message section, where no block result carries the edit. */
84
+ export function renameProjectMessage(project, workspace, id, next) {
85
+ if (workspace.options.readOnly || workspace.isDragging())
86
+ return 'read-only';
87
+ const owner = project.targets.find(target => Object.hasOwn(target.variables, id));
88
+ const variable = owner?.variables[id];
89
+ if (variable?.type !== 'broadcast_msg' || workspace.getVariableById(id)?.name !== variable.name)
90
+ return 'stale';
91
+ if (!validName(next))
92
+ return 'invalid';
93
+ const match = next.toLocaleLowerCase();
94
+ if (project.targets.some(target => Object.entries(target.variables).some(([other, candidate]) => other !== id && candidate.type === 'broadcast_msg' && candidate.name.toLocaleLowerCase() === match))) {
95
+ return 'conflict';
96
+ }
97
+ maintainBroadcastReferences(project, workspace, id);
98
+ workspace.renameVariableById(id, next);
99
+ return workspace.getVariableById(id)?.name === next ? 'applied' : 'conflict';
100
+ }
101
+ export function applyProjectSearchReplacement(project, workspace, Blockly, result, next) {
102
+ if (workspace.options.readOnly || workspace.isDragging() || !result.edit)
103
+ return 'read-only';
104
+ const { target, edit } = result;
105
+ if (!project.targets.includes(target) || ('block' in result && target.blocks.getBlock(result.block.id) !== result.block))
106
+ return 'stale';
107
+ if (edit.type === 'comment' && result.kind === 'comment') {
108
+ const current = target.comments?.[result.commentId];
109
+ if (current !== result.comment || current.text !== edit.text)
110
+ return 'stale';
111
+ const comment = current.blockId ? workspace.getBlockById(current.blockId)?.comment : workspace.getCommentById(result.commentId);
112
+ if (!comment || comment.getText() !== edit.text)
113
+ return 'stale';
114
+ comment.setText(next);
115
+ return 'applied';
116
+ }
117
+ if (edit.type === 'name') {
118
+ if (!('block' in result))
119
+ return 'stale';
120
+ const fieldName = result.kind === 'variable' ? 'VARIABLE' : result.kind === 'list' ? 'LIST' : 'BROADCAST_OPTION';
121
+ const field = result.block.fields?.[fieldName] ??
122
+ target.blocks.getBlock(result.block.inputs?.BROADCAST_INPUT?.block)?.fields?.[fieldName];
123
+ if (field?.id !== edit.id)
124
+ return 'stale';
125
+ const variable = edit.owner.variables[edit.id];
126
+ const model = workspace.getVariableById(edit.id);
127
+ if (!project.targets.includes(edit.owner) || !variable || variable.name !== edit.name || model?.name !== edit.name)
128
+ return 'stale';
129
+ if (!validName(next) || variable.isCloud)
130
+ return 'invalid';
131
+ const scope = edit.owner.isStage ? project.targets.filter(target => target.isOriginal) :
132
+ project.targets.filter(target => target === edit.owner || target.isStage);
133
+ const match = next.toLocaleLowerCase();
134
+ if (scope.some(target => Object.entries(target.variables).some(([id, candidate]) => id !== edit.id && (candidate.type ?? '') === edit.variableType && candidate.name.toLocaleLowerCase() === match)))
135
+ return 'conflict';
136
+ if (edit.variableType === 'broadcast_msg')
137
+ maintainBroadcastReferences(project, workspace, edit.id);
138
+ workspace.renameVariableById(edit.id, next);
139
+ return workspace.getVariableById(edit.id)?.name === next ? 'applied' : 'conflict';
140
+ }
141
+ if (edit.type === 'procedure') {
142
+ if (!('block' in result))
143
+ return 'stale';
144
+ const procedure = result.block.mutation?.proccode ??
145
+ target.blocks.getBlock(result.block.inputs?.custom_block?.block)?.mutation?.proccode;
146
+ if (procedure !== edit.name)
147
+ return 'stale';
148
+ if (!validName(next) || !next.replace(/%[sbn]/g, '').trim())
149
+ return 'invalid';
150
+ const prototype = Blockly.Procedures.getPrototypeBlock(edit.name, workspace);
151
+ if (!prototype)
152
+ return 'stale';
153
+ if (next !== edit.name && Blockly.Procedures.getPrototypeBlock(next, workspace))
154
+ return 'conflict';
155
+ const mutation = prototype.mutationToDom();
156
+ mutation.setAttribute('proccode', next);
157
+ Blockly.Procedures.mutateCallersAndPrototype(edit.name, workspace, mutation);
158
+ return 'applied';
159
+ }
160
+ if (edit.type === 'field' && 'block' in result) {
161
+ const { id, field: name, value } = edit;
162
+ if (id !== result.block.id && !Object.values(result.block.inputs ?? {}).some(input => input.block === id))
163
+ return 'stale';
164
+ const block = workspace.getBlockById(id);
165
+ const field = block?.getField(name);
166
+ if (!field || String(field.getValue()) !== value || String(target.blocks.getBlock(id)?.fields?.[name]?.value) !== value)
167
+ return 'stale';
168
+ const text = Boolean(field instanceof Blockly.FieldTextInput);
169
+ const dropdown = Boolean(field instanceof Blockly.FieldDropdown);
170
+ if (!block?.isEditable() || !(text || dropdown))
171
+ return 'read-only';
172
+ if (field instanceof Blockly.FieldNumber && (!next.trim() || !Number.isFinite(Number(next))))
173
+ return 'invalid';
174
+ if (dropdown && !field.getOptions?.().some(option => String(option[1]) === next))
175
+ return 'invalid';
176
+ const validated = field.callValidator(next);
177
+ if (validated === null || (validated !== undefined && String(validated) !== next))
178
+ return 'invalid';
179
+ field.setValue(next);
180
+ return String(field.getValue()) === next ? 'applied' : 'invalid';
181
+ }
182
+ return 'read-only';
183
+ }
@@ -0,0 +1,136 @@
1
+ type Field = {
2
+ id?: string;
3
+ value: unknown;
4
+ };
5
+ type Block = {
6
+ id: string;
7
+ opcode: string;
8
+ next?: string;
9
+ shadow?: boolean;
10
+ fields?: Record<string, Field>;
11
+ inputs?: Record<string, {
12
+ block?: string;
13
+ shadow?: string;
14
+ }>;
15
+ mutation?: {
16
+ proccode?: string;
17
+ blockInfo?: {
18
+ text?: string;
19
+ };
20
+ };
21
+ };
22
+ type Asset = {
23
+ name: string;
24
+ };
25
+ type Comment = {
26
+ text: string;
27
+ blockId?: string | null;
28
+ };
29
+ type Variable = {
30
+ name: string;
31
+ type?: string;
32
+ isCloud?: boolean;
33
+ value?: unknown;
34
+ };
35
+ type Target = {
36
+ id: string;
37
+ isStage: boolean;
38
+ isOriginal: boolean;
39
+ getName(): string;
40
+ getCostumes(): Asset[];
41
+ getSounds(): Asset[];
42
+ variables: Record<string, Variable>;
43
+ comments?: Record<string, Comment>;
44
+ blocks: {
45
+ getScripts(): string[];
46
+ getBlock(id: string | undefined): Block | undefined;
47
+ };
48
+ };
49
+ export type SearchProject = {
50
+ targets: Target[];
51
+ getLabelForOpcode?(opcode: string): {
52
+ category?: string;
53
+ label?: string;
54
+ } | undefined;
55
+ resetAllCaches?(): void;
56
+ emitProjectChanged?(): void;
57
+ on?(event: 'PROJECT_CHANGED', listener: () => void): void;
58
+ removeListener?(event: 'PROJECT_CHANGED', listener: () => void): void;
59
+ };
60
+ type BlockKind = 'block' | 'variable' | 'list' | 'message' | 'argument' | 'extension';
61
+ type AssetKind = 'costume' | 'backdrop' | 'sound';
62
+ export type SearchKind = BlockKind | AssetKind | 'comment' | 'yaml';
63
+ export type VariableAccess = 'read' | 'write';
64
+ export type VariableSearch = {
65
+ ownerId: string;
66
+ variableId: string;
67
+ access: VariableAccess;
68
+ };
69
+ export declare const variableAccessLabels: {
70
+ readonly read: readonly ["読み取り", "Read"];
71
+ readonly write: readonly ["変更", "Write"];
72
+ };
73
+ export type SearchEdit = {
74
+ type: 'field';
75
+ id: string;
76
+ field: string;
77
+ value: string;
78
+ } | {
79
+ type: 'name';
80
+ id: string;
81
+ owner: Target;
82
+ name: string;
83
+ variableType: string;
84
+ } | {
85
+ type: 'procedure';
86
+ name: string;
87
+ } | {
88
+ type: 'comment';
89
+ text: string;
90
+ };
91
+ export type ProjectSearchResult = {
92
+ label: string;
93
+ target: Target;
94
+ detail?: string;
95
+ edit?: SearchEdit;
96
+ } & ({
97
+ kind: BlockKind;
98
+ block: Block;
99
+ variableAccess?: VariableAccess;
100
+ } | {
101
+ kind: AssetKind;
102
+ asset: Asset;
103
+ } | {
104
+ kind: 'comment';
105
+ comment: Comment;
106
+ commentId: string;
107
+ });
108
+ export type YamlSearchDocument = {
109
+ reference: {
110
+ providerId: string;
111
+ documentId: string;
112
+ };
113
+ filename: string;
114
+ source: string;
115
+ };
116
+ export type YamlSearchResult = {
117
+ kind: 'yaml';
118
+ document: YamlSearchDocument;
119
+ label: string;
120
+ detail: string;
121
+ range: {
122
+ start: number;
123
+ end: number;
124
+ expectedText: string;
125
+ };
126
+ };
127
+ export type SearchResult = ProjectSearchResult | YamlSearchResult;
128
+ export declare const literalSearchPattern: (query: string, flags: string) => RegExp;
129
+ export declare function countVariableAccesses(project: SearchProject, ownerId: string): Map<string, Record<VariableAccess, number>>;
130
+ export declare function searchProject(project: SearchProject, messages: Record<string, string>, query: string, japanese: boolean, kinds?: ReadonlySet<SearchKind>, variableSearch?: VariableSearch): {
131
+ kind: SearchKind;
132
+ title: string;
133
+ results: ProjectSearchResult[];
134
+ }[];
135
+ export declare function searchYamlDocuments(documents: readonly YamlSearchDocument[], query: string, japanese: boolean): YamlSearchResult[];
136
+ export {};