UNPKG

14.3 kBPlain TextView Raw
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import * as Common from '../../core/common/common.js';
6import * as SDK from '../../core/sdk/sdk.js';
7import * as Bindings from '../bindings/bindings.js';
8import * as TextUtils from '../text_utils/text_utils.js';
9import * as Workspace from '../workspace/workspace.js';
10
11import type {FormatterSourceMapping} from './ScriptFormatter.js';
12import {format} from './ScriptFormatter.js';
13
14const objectToFormattingResult = new WeakMap<Object, SourceFormatData>();
15
16export 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
38let sourceFormatterInstance: SourceFormatter|null = null;
39
40export 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 /* isServiceProject */);
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, /* metadata */ 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
157class 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
247const sourceCodeToHeaders =
248 new WeakMap<Workspace.UISourceCode.UISourceCode, SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[]>();
249
250class 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