1 |
|
2 |
|
3 |
|
4 |
|
5 | import * as Common from '../../core/common/common.js';
|
6 | import * as SDK from '../../core/sdk/sdk.js';
|
7 | import * as Bindings from '../bindings/bindings.js';
|
8 | import * as TextUtils from '../text_utils/text_utils.js';
|
9 | import * as Workspace from '../workspace/workspace.js';
|
10 |
|
11 | import type {FormatterSourceMapping} from './ScriptFormatter.js';
|
12 | import {format} from './ScriptFormatter.js';
|
13 |
|
14 | const objectToFormattingResult = new WeakMap<Object, SourceFormatData>();
|
15 |
|
16 | export class SourceFormatData {
|
17 | originalSourceCode: Workspace.UISourceCode.UISourceCode;
|
18 | formattedSourceCode: Workspace.UISourceCode.UISourceCode;
|
19 | mapping: FormatterSourceMapping;
|
20 |
|
21 | constructor(
|
22 | originalSourceCode: Workspace.UISourceCode.UISourceCode, formattedSourceCode: Workspace.UISourceCode.UISourceCode,
|
23 | mapping: FormatterSourceMapping) {
|
24 | this.originalSourceCode = originalSourceCode;
|
25 | this.formattedSourceCode = formattedSourceCode;
|
26 | this.mapping = mapping;
|
27 | }
|
28 |
|
29 | originalPath(): string {
|
30 | return this.originalSourceCode.project().id() + ':' + this.originalSourceCode.url();
|
31 | }
|
32 |
|
33 | static for(object: Object): SourceFormatData|null {
|
34 | return objectToFormattingResult.get(object) || null;
|
35 | }
|
36 | }
|
37 |
|
38 | let sourceFormatterInstance: SourceFormatter|null = null;
|
39 |
|
40 | export class SourceFormatter {
|
41 | private readonly projectId: string;
|
42 | private readonly project: Bindings.ContentProviderBasedProject.ContentProviderBasedProject;
|
43 | private readonly formattedSourceCodes: Map<Workspace.UISourceCode.UISourceCode, {
|
44 | promise: Promise<SourceFormatData>,
|
45 | formatData: SourceFormatData|null,
|
46 | }>;
|
47 | private readonly scriptMapping: ScriptMapping;
|
48 | private readonly styleMapping: StyleMapping;
|
49 |
|
50 | constructor() {
|
51 | this.projectId = 'formatter:';
|
52 | this.project = new Bindings.ContentProviderBasedProject.ContentProviderBasedProject(
|
53 | Workspace.Workspace.WorkspaceImpl.instance(), this.projectId, Workspace.Workspace.projectTypes.Formatter,
|
54 | 'formatter', true );
|
55 |
|
56 | this.formattedSourceCodes = new Map();
|
57 | this.scriptMapping = new ScriptMapping();
|
58 | this.styleMapping = new StyleMapping();
|
59 | Workspace.Workspace.WorkspaceImpl.instance().addEventListener(
|
60 | Workspace.Workspace.Events.UISourceCodeRemoved, event => {
|
61 | this.onUISourceCodeRemoved(event);
|
62 | }, this);
|
63 | }
|
64 |
|
65 | static instance({forceNew = false}: {forceNew?: boolean} = {}): SourceFormatter {
|
66 | if (!sourceFormatterInstance || forceNew) {
|
67 | sourceFormatterInstance = new SourceFormatter();
|
68 | }
|
69 | return sourceFormatterInstance;
|
70 | }
|
71 |
|
72 | private async onUISourceCodeRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>):
|
73 | Promise<void> {
|
74 | const uiSourceCode = event.data;
|
75 | const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
|
76 | if (cacheEntry && cacheEntry.formatData) {
|
77 | await this.discardFormatData(cacheEntry.formatData);
|
78 | }
|
79 | this.formattedSourceCodes.delete(uiSourceCode);
|
80 | }
|
81 |
|
82 | async discardFormattedUISourceCode(formattedUISourceCode: Workspace.UISourceCode.UISourceCode):
|
83 | Promise<Workspace.UISourceCode.UISourceCode|null> {
|
84 | const formatData = SourceFormatData.for(formattedUISourceCode);
|
85 | if (!formatData) {
|
86 | return null;
|
87 | }
|
88 | await this.discardFormatData(formatData);
|
89 | this.formattedSourceCodes.delete(formatData.originalSourceCode);
|
90 | return formatData.originalSourceCode;
|
91 | }
|
92 |
|
93 | private async discardFormatData(formatData: SourceFormatData): Promise<void> {
|
94 | objectToFormattingResult.delete(formatData.formattedSourceCode);
|
95 | await this.scriptMapping.setSourceMappingEnabled(formatData, false);
|
96 | this.styleMapping.setSourceMappingEnabled(formatData, false);
|
97 | this.project.removeFile(formatData.formattedSourceCode.url());
|
98 | }
|
99 |
|
100 | hasFormatted(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
|
101 | return this.formattedSourceCodes.has(uiSourceCode);
|
102 | }
|
103 |
|
104 | getOriginalUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode {
|
105 | const formatData = objectToFormattingResult.get(uiSourceCode);
|
106 | if (!formatData) {
|
107 | return uiSourceCode;
|
108 | }
|
109 | return formatData.originalSourceCode;
|
110 | }
|
111 |
|
112 | async format(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<SourceFormatData> {
|
113 | const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
|
114 | if (cacheEntry) {
|
115 | return cacheEntry.promise;
|
116 | }
|
117 |
|
118 | const resultPromise = new Promise<SourceFormatData>(async (resolve, reject) => {
|
119 | const {content} = await uiSourceCode.requestContent();
|
120 |
|
121 | try {
|
122 | const {formattedContent, formattedMapping} =
|
123 | await format(uiSourceCode.contentType(), uiSourceCode.mimeType(), content || '');
|
124 | const cacheEntry = this.formattedSourceCodes.get(uiSourceCode);
|
125 | if (!cacheEntry || cacheEntry.promise !== resultPromise) {
|
126 | return;
|
127 | }
|
128 | let formattedURL;
|
129 | let count = 0;
|
130 | let suffix = '';
|
131 | do {
|
132 | formattedURL = `${uiSourceCode.url()}:formatted${suffix}`;
|
133 | suffix = `:${count++}`;
|
134 | } while (this.project.uiSourceCodeForURL(formattedURL));
|
135 | const contentProvider = TextUtils.StaticContentProvider.StaticContentProvider.fromString(
|
136 | formattedURL, uiSourceCode.contentType(), formattedContent);
|
137 | const formattedUISourceCode = this.project.createUISourceCode(formattedURL, contentProvider.contentType());
|
138 | const formatData = new SourceFormatData(uiSourceCode, formattedUISourceCode, formattedMapping);
|
139 | objectToFormattingResult.set(formattedUISourceCode, formatData);
|
140 | this.project.addUISourceCodeWithProvider(
|
141 | formattedUISourceCode, contentProvider, null, uiSourceCode.mimeType());
|
142 | await this.scriptMapping.setSourceMappingEnabled(formatData, true);
|
143 | await this.styleMapping.setSourceMappingEnabled(formatData, true);
|
144 | cacheEntry.formatData = formatData;
|
145 | resolve(formatData);
|
146 | } catch (e) {
|
147 | reject(e);
|
148 | }
|
149 | });
|
150 |
|
151 | this.formattedSourceCodes.set(uiSourceCode, {promise: resultPromise, formatData: null});
|
152 |
|
153 | return resultPromise;
|
154 | }
|
155 | }
|
156 |
|
157 | class ScriptMapping implements Bindings.DebuggerWorkspaceBinding.DebuggerSourceMapping {
|
158 | constructor() {
|
159 | Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().addSourceMapping(this);
|
160 | }
|
161 |
|
162 | rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
|
163 | const script = rawLocation.script();
|
164 | const formatData = script && SourceFormatData.for(script);
|
165 | if (!formatData || !script) {
|
166 | return null;
|
167 | }
|
168 | const [lineNumber, columnNumber] =
|
169 | formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0);
|
170 | return formatData.formattedSourceCode.uiLocation(lineNumber, columnNumber);
|
171 | }
|
172 |
|
173 | uiLocationToRawLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number):
|
174 | SDK.DebuggerModel.Location[] {
|
175 | const formatData = SourceFormatData.for(uiSourceCode);
|
176 | if (!formatData) {
|
177 | return [];
|
178 | }
|
179 | const [originalLine, originalColumn] = formatData.mapping.formattedToOriginal(lineNumber, columnNumber);
|
180 | if (formatData.originalSourceCode.contentType().isScript()) {
|
181 | // Here we have a script that is displayed on its own (i.e. it has a dedicated uiSourceCode). This means it is
|
182 | // either a stand-alone script or an inline script with a #sourceURL= and in both cases we can just forward the
|
183 | // question to the original (unformatted) source code.
|
184 | const rawLocations = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
|
185 | .uiLocationToRawLocationsForUnformattedJavaScript(
|
186 | formatData.originalSourceCode, originalLine, originalColumn);
|
187 | console.assert(rawLocations.every(l => l && Boolean(l.script())));
|
188 | return rawLocations;
|
189 | }
|
190 | if (formatData.originalSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
|
191 | const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(formatData.originalSourceCode);
|
192 | const debuggerModel = target && target.model(SDK.DebuggerModel.DebuggerModel);
|
193 | if (debuggerModel) {
|
194 | const scripts = debuggerModel.scriptsForSourceURL(formatData.originalSourceCode.url())
|
195 | .filter(script => script.isInlineScript() && !script.hasSourceURL);
|
196 | // Here we have an inline script, which was formatted together with the containing document, so we must not
|
197 | // translate locations as they are relative to the start of the document.
|
198 | const locations =
|
199 | (scripts.map(script => script.rawLocation(originalLine, originalColumn)).filter(l => Boolean(l)) as
|
200 | SDK.DebuggerModel.Location[]);
|
201 | console.assert(locations.every(l => l && Boolean(l.script())));
|
202 | return locations;
|
203 | }
|
204 | }
|
205 | return [];
|
206 | }
|
207 |
|
208 | async setSourceMappingEnabled(formatData: SourceFormatData, enabled: boolean): Promise<void> {
|
209 | const scripts = this.scriptsForUISourceCode(formatData.originalSourceCode);
|
210 | if (!scripts.length) {
|
211 | return;
|
212 | }
|
213 | if (enabled) {
|
214 | for (const script of scripts) {
|
215 | objectToFormattingResult.set(script, formatData);
|
216 | }
|
217 | } else {
|
218 | for (const script of scripts) {
|
219 | objectToFormattingResult.delete(script);
|
220 | }
|
221 | }
|
222 | const updatePromises = scripts.map(
|
223 | script => Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().updateLocations(script));
|
224 | await Promise.all(updatePromises);
|
225 | }
|
226 |
|
227 | private scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
|
228 | if (uiSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
|
229 | const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
|
230 | const debuggerModel = target && target.model(SDK.DebuggerModel.DebuggerModel);
|
231 | if (debuggerModel) {
|
232 | const scripts = debuggerModel.scriptsForSourceURL(uiSourceCode.url())
|
233 | .filter(script => script.isInlineScript() && !script.hasSourceURL);
|
234 | return scripts;
|
235 | }
|
236 | }
|
237 | if (uiSourceCode.contentType().isScript()) {
|
238 | console.assert(!objectToFormattingResult.has(uiSourceCode));
|
239 | const rawLocations = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
|
240 | .uiLocationToRawLocationsForUnformattedJavaScript(uiSourceCode, 0, 0);
|
241 | return rawLocations.map(location => location.script()).filter(script => Boolean(script)) as SDK.Script.Script[];
|
242 | }
|
243 | return [];
|
244 | }
|
245 | }
|
246 |
|
247 | const sourceCodeToHeaders =
|
248 | new WeakMap<Workspace.UISourceCode.UISourceCode, SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[]>();
|
249 |
|
250 | class StyleMapping implements Bindings.CSSWorkspaceBinding.SourceMapping {
|
251 | private readonly headersSymbol: symbol;
|
252 | constructor() {
|
253 | Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().addSourceMapping(this);
|
254 | this.headersSymbol = Symbol('Formatter.SourceFormatter.StyleMapping._headersSymbol');
|
255 | }
|
256 |
|
257 | rawLocationToUILocation(rawLocation: SDK.CSSModel.CSSLocation): Workspace.UISourceCode.UILocation|null {
|
258 | const styleHeader = rawLocation.header();
|
259 | const formatData = styleHeader && SourceFormatData.for(styleHeader);
|
260 | if (!formatData) {
|
261 | return null;
|
262 | }
|
263 | const formattedLocation =
|
264 | formatData.mapping.originalToFormatted(rawLocation.lineNumber, rawLocation.columnNumber || 0);
|
265 | return formatData.formattedSourceCode.uiLocation(formattedLocation[0], formattedLocation[1]);
|
266 | }
|
267 |
|
268 | uiLocationToRawLocations(uiLocation: Workspace.UISourceCode.UILocation): SDK.CSSModel.CSSLocation[] {
|
269 | const formatData = SourceFormatData.for(uiLocation.uiSourceCode);
|
270 | if (!formatData) {
|
271 | return [];
|
272 | }
|
273 | const [originalLine, originalColumn] =
|
274 | formatData.mapping.formattedToOriginal(uiLocation.lineNumber, uiLocation.columnNumber);
|
275 | const allHeaders = sourceCodeToHeaders.get(formatData.originalSourceCode);
|
276 |
|
277 | if (!allHeaders) {
|
278 | return [];
|
279 | }
|
280 |
|
281 | const headers = allHeaders.filter(header => header.containsLocation(originalLine, originalColumn));
|
282 | return headers.map(header => new SDK.CSSModel.CSSLocation(header, originalLine, originalColumn));
|
283 | }
|
284 |
|
285 | async setSourceMappingEnabled(formatData: SourceFormatData, enable: boolean): Promise<void> {
|
286 | const original = formatData.originalSourceCode;
|
287 | const headers = this.headersForUISourceCode(original);
|
288 | if (enable) {
|
289 | sourceCodeToHeaders.set(original, headers);
|
290 | headers.forEach(header => {
|
291 | objectToFormattingResult.set(header, formatData);
|
292 | });
|
293 | } else {
|
294 | sourceCodeToHeaders.delete(original);
|
295 | headers.forEach(header => {
|
296 | objectToFormattingResult.delete(header);
|
297 | });
|
298 | }
|
299 | const updatePromises =
|
300 | headers.map(header => Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().updateLocations(header));
|
301 | await Promise.all(updatePromises);
|
302 | }
|
303 |
|
304 | private headersForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode):
|
305 | SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] {
|
306 | if (uiSourceCode.contentType() === Common.ResourceType.resourceTypes.Document) {
|
307 | const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
|
308 | const cssModel = target && target.model(SDK.CSSModel.CSSModel);
|
309 | if (cssModel) {
|
310 | return cssModel.headersForSourceURL(uiSourceCode.url())
|
311 | .filter(header => header.isInline && !header.hasSourceURL);
|
312 | }
|
313 | } else if (uiSourceCode.contentType().isStyleSheet()) {
|
314 | const rawLocations = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().uiLocationToRawLocations(
|
315 | uiSourceCode.uiLocation(0, 0));
|
316 | return rawLocations.map(rawLocation => rawLocation.header()).filter(header => Boolean(header)) as
|
317 | SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[];
|
318 | }
|
319 | return [];
|
320 | }
|
321 | }
|
322 |
|
\ | No newline at end of file |