Bug 1945467 - [devtools] No longer using getScopes for inline previews in CM6 r=ochameau

Highlights of this patch
- Added `getBindingScopeReferencesFinder` returns a finder function which would be used to
get the location and meta details for any binding reference with the scope of the immediate function containing the specified location

- Use the platform scopes to determine the scope levels with `getScopeLevels`

Differential Revision: https://phabricator.services.mozilla.com/D237564
This commit is contained in:
Hubert Boma Manilla
2025-03-27 09:24:41 +00:00
parent 5f2238c730
commit 312e51a9e4
4 changed files with 265 additions and 76 deletions

View File

@@ -8,6 +8,7 @@ import {
getSelectedScope, getSelectedScope,
} from "../../selectors/index"; } from "../../selectors/index";
import { features } from "../../utils/prefs"; import { features } from "../../utils/prefs";
import { getEditor } from "../../utils/editor/index";
import { validateSelectedFrame } from "../../utils/context"; import { validateSelectedFrame } from "../../utils/context";
/** /**
@@ -24,8 +25,8 @@ export function generateInlinePreview(selectedFrame) {
if (getSelectedFrameInlinePreviews(getState())) { if (getSelectedFrameInlinePreviews(getState())) {
return null; return null;
} }
const scope = getSelectedScope(getState());
const scope = getSelectedScope(getState());
if (!scope || !scope.bindings) { if (!scope || !scope.bindings) {
return null; return null;
} }
@@ -52,6 +53,8 @@ export function generateInlinePreview(selectedFrame) {
previews[line].push(preview); previews[line].push(preview);
} }
validateSelectedFrame(getState(), selectedFrame);
return dispatch({ return dispatch({
type: "ADD_INLINE_PREVIEW", type: "ADD_INLINE_PREVIEW",
selectedFrame, selectedFrame,
@@ -63,7 +66,7 @@ export function generateInlinePreview(selectedFrame) {
* Creates all the previews * Creates all the previews
* *
* @param {Object} selectedFrame * @param {Object} selectedFrame
* @param {Object} scope - Scopes from the platform * @param {Object} scope - Scope from the platform
* @param {Object} thunkArgs * @param {Object} thunkArgs
* @returns * @returns
*/ */
@@ -81,46 +84,79 @@ async function getPreviews(selectedFrame, scope, thunkArgs) {
return []; return [];
} }
const originalAstScopes = await parserWorker.getScopes(selectedLocation);
if (!originalAstScopes) {
return [];
}
// Bailout if we resumed or moved to another frame while computing the scope
validateSelectedFrame(getState(), selectedFrame);
const allPreviews = []; const allPreviews = [];
let level = 0; if (features.codemirrorNext) {
while (scope && scope.bindings) { // Get all the bindings for all scopes up until and including the first function scope.
// All the bindings from the platform environment let allBindings = {};
const bindings = getScopeBindings(scope); while (scope && scope.bindings) {
const bindings = getScopeBindings(scope);
allBindings = { ...allBindings, ...bindings };
if (scope.type === "function") {
break;
}
scope = scope.parent;
}
const editor = getEditor(features.codemirrorNext);
const references = await editor.getBindingReferences(
selectedLocation,
Object.keys(allBindings)
);
// Generate the previews for all the bindings validateSelectedFrame(getState(), selectedFrame);
const allPreviewBindingsComplete = Object.keys(bindings).map(async name => {
// Get previews for this binding for (const name in references) {
const previews = await generatePreviewsForBinding( const previews = await generatePreviewsForBinding(
originalAstScopes[level]?.bindings[name], references[name],
selectedLocation.line, selectedLocation.line,
name, name,
bindings[name].value, allBindings[name].value,
client, client,
selectedFrame.thread selectedFrame.thread
); );
allPreviews.push(...previews); allPreviews.push(...previews);
});
await Promise.all(allPreviewBindingsComplete);
// Bailout if we resumed or moved to another frame while fetching the values from the backend
validateSelectedFrame(getState(), selectedFrame);
// We need to display all variables in for all block scopes up until
// and including the first function scope.
if (scope.type === "function") {
break;
} }
level++; } else {
scope = scope.parent; const originalAstScopes = await parserWorker.getScopes(selectedLocation);
if (!originalAstScopes) {
return [];
}
// Bailout if we resumed or moved to another frame while computing the scope
validateSelectedFrame(getState(), selectedFrame);
let level = 0;
while (scope && scope.bindings) {
// All the bindings from the platform environment
const bindings = getScopeBindings(scope);
// Generate the previews for all the bindings
const allPreviewBindingsComplete = Object.keys(bindings).map(
async name => {
// Get previews for this binding
const previews = await generatePreviewsForBinding(
originalAstScopes[level]?.bindings[name],
selectedLocation.line,
name,
bindings[name].value,
client,
selectedFrame.thread
);
allPreviews.push(...previews);
}
);
await Promise.all(allPreviewBindingsComplete);
// Bailout if we resumed or moved to another frame while fetching the values from the backend
validateSelectedFrame(getState(), selectedFrame);
// We need to display all variables in for all block scopes up until
// and including the first function scope.
if (scope.type === "function") {
break;
}
level++;
scope = scope.parent;
}
} }
return allPreviews; return allPreviews;
} }

View File

@@ -1047,7 +1047,8 @@ class Editor extends EventEmitter {
// investigate further Bug 1890895. // investigate further Bug 1890895.
event.target.ownerGlobal.setTimeout(() => { event.target.ownerGlobal.setTimeout(() => {
const view = editor.viewState; const view = editor.viewState;
const cursorPos = this.#posToLineColumn( const cursorPos = lezerUtils.positionToLocation(
view.state.doc,
view.state.selection.main.head view.state.selection.main.head
); );
handler(event, view, cursorPos.line, cursorPos.column); handler(event, view, cursorPos.line, cursorPos.column);
@@ -1868,12 +1869,16 @@ class Editor extends EventEmitter {
const { const {
codemirrorLanguage: { syntaxTree }, codemirrorLanguage: { syntaxTree },
} = this.#CodeMirror6; } = this.#CodeMirror6;
const lineObject = cm.state.doc.line(line);
const pos = lineObject.from + column; const token = lezerUtils.getTreeNodeAtLocation(
const token = syntaxTree(cm.state).resolve(pos, 1); cm.state.doc,
syntaxTree(cm.state),
{ line, column }
);
if (!token) { if (!token) {
return null; return null;
} }
return { return {
startColumn: column, startColumn: column,
endColumn: token.to - token.from, endColumn: token.to - token.from,
@@ -2150,7 +2155,10 @@ class Editor extends EventEmitter {
handle: { handle: {
markedSpans: markedSpans markedSpans: markedSpans
? markedSpans.map(span => { ? markedSpans.map(span => {
const { column } = this.#posToLineColumn(cm.posAtDOM(span)); const { column } = lezerUtils.positionToLocation(
cm.state.doc,
cm.posAtDOM(span)
);
return { return {
marker: { className: span.className }, marker: { className: span.className },
from: column, from: column,
@@ -2198,8 +2206,8 @@ class Editor extends EventEmitter {
name, name,
klass: lezerUtils.getFunctionClass(cm.state.doc, syntaxNode), klass: lezerUtils.getFunctionClass(cm.state.doc, syntaxNode),
location: { location: {
start: this.#posToLineColumn(node.from), start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: this.#posToLineColumn(node.to), end: lezerUtils.positionToLocation(cm.state.doc, node.to),
}, },
parameterNames: lezerUtils.getFunctionParameterNames( parameterNames: lezerUtils.getFunctionParameterNames(
cm.state.doc, cm.state.doc,
@@ -2236,8 +2244,8 @@ class Editor extends EventEmitter {
classVarDefNode.to classVarDefNode.to
), ),
location: { location: {
start: this.#posToLineColumn(node.from), start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: this.#posToLineColumn(node.to), end: lezerUtils.positionToLocation(cm.state.doc, node.to),
}, },
}); });
}, },
@@ -2290,8 +2298,14 @@ class Editor extends EventEmitter {
doc = cm.state.toText(sourceContent); doc = cm.state.toText(sourceContent);
tree = lezerUtils.getTree(javascriptLanguage, sourceId, sourceContent); tree = lezerUtils.getTree(javascriptLanguage, sourceId, sourceContent);
} }
const token = lezerUtils.getTreeNodeAtLocation(doc, tree, location); const token = lezerUtils.getTreeNodeAtLocation(doc, tree, location);
return lezerUtils.getEnclosingFunctionName(doc, token); if (!token) {
return null;
}
const enclosingScope = lezerUtils.getEnclosingFunction(doc, token);
return enclosingScope ? enclosingScope.funcName : "";
} }
/** /**
@@ -2321,8 +2335,8 @@ class Editor extends EventEmitter {
computed: false, computed: false,
expression: cm.state.doc.sliceString(node.from, node.to), expression: cm.state.doc.sliceString(node.from, node.to),
location: { location: {
start: this.#posToLineColumn(node.from), start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: this.#posToLineColumn(node.to), end: lezerUtils.positionToLocation(cm.state.doc, node.to),
}, },
from: node.from, from: node.from,
to: node.to, to: node.to,
@@ -2435,6 +2449,73 @@ class Editor extends EventEmitter {
return sourceLines.filter(i => i != undefined); return sourceLines.filter(i => i != undefined);
} }
/**
* Gets the location and meta details of all the references
* (within the scope of the immediate enclosing function of the specified location)
* which are related to the bindings specified.
*
* @param {Object} location
* @param {Array<String>} bindings - list of binding names
* @returns {Function}
**/
async getBindingReferences(location, bindings) {
const cm = editors.get(this);
const {
codemirrorLanguage: { syntaxTree },
} = this.#CodeMirror6;
const token = lezerUtils.getTreeNodeAtLocation(
cm.state.doc,
syntaxTree(cm.state),
location
);
if (!token) {
return null;
}
const enclosingScope = lezerUtils.getEnclosingFunction(
cm.state.doc,
token,
{ includeAnonymousFunctions: true }
);
if (!enclosingScope) {
return null;
}
const bindingReferences = {};
// This should find location and meta information for the binding name specified.
await lezerUtils.walkCursor(enclosingScope.node.cursor(), {
filterSet: lezerUtils.nodeTypeSets.bindingReferences,
enterVisitor: node => {
const bindingName = cm.state.doc.sliceString(node.from, node.to);
if (!bindings.includes(bindingName)) {
return;
}
const ref = {
start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: lezerUtils.positionToLocation(cm.state.doc, node.to),
};
const syntaxNode = node.node;
// Previews for member expressions are built of the meta property which is
// reference of the child property and so on. e.g a.b.c
if (syntaxNode.parent.name == lezerUtils.nodeTypes.MemberExpression) {
ref.meta = lezerUtils.getMetaBindings(
cm.state.doc,
syntaxNode.parent
);
}
if (!bindingReferences[bindingName]) {
bindingReferences[bindingName] = { refs: [] };
}
bindingReferences[bindingName].refs.push(ref);
},
});
return bindingReferences;
}
/** /**
* Replaces whatever is in the text area with the contents of * Replaces whatever is in the text area with the contents of
* the 'value' argument. * the 'value' argument.
@@ -3396,27 +3477,6 @@ class Editor extends EventEmitter {
return inXView && inYView; return inXView && inYView;
} }
/**
* Determines the line and column values the map to the codemirror offset specified.
* Used only for CM6
* @param {Number} pos - Codemirror offset
* @returns {Object} - Line column related to the position
*/
#posToLineColumn(pos) {
const cm = editors.get(this);
if (pos == null) {
return {
line: null,
column: null,
};
}
const line = cm.state.doc.lineAt(pos);
return {
line: line.number,
column: pos - line.from,
};
}
/** /**
* Converts line/col to CM6 offset position * Converts line/col to CM6 offset position
* @param {Number} line - The line in the source * @param {Number} line - The line in the source
@@ -3617,7 +3677,10 @@ class Editor extends EventEmitter {
return { text: "", line: -1, column: -1 }; return { text: "", line: -1, column: -1 };
} }
const cursorPosition = this.#posToLineColumn(cursor.to); const cursorPosition = lezerUtils.positionToLocation(
cm.state.doc,
cursor.to
);
// The lines in CM6 are 1 based while CM5 is 0 based // The lines in CM6 are 1 based while CM5 is 0 based
return { return {
text: cursor.match[0], text: cursor.match[0],

View File

@@ -71,6 +71,11 @@ const nodeTypeSets = {
numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]), numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]),
memberExpression: new Set([nodeTypes.MemberExpression]), memberExpression: new Set([nodeTypes.MemberExpression]),
classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]), classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]),
bindingReferences: new Set([
nodeTypes.VariableDefinition,
nodeTypes.VariableName,
]),
expressionProperty: new Set([nodeTypes.PropertyName]),
}; };
const ast = new Map(); const ast = new Map();
@@ -133,24 +138,41 @@ function clear() {
} }
/** /**
* Gets the name of the function which immediately encloses the node (representing a location) * Gets the node and the function name which immediately encloses the node (representing a location)
* *
* @param {Object} doc - The codemirror document used to retrive the part of content * @param {Object} doc - The codemirror document used to retrive the part of content
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode * @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
* @params {Object} options
* options.includeAnonymousFunctions - if true, allow matching anonymous functions
* @returns * @returns
*/ */
function getEnclosingFunctionName(doc, node) { function getEnclosingFunction(
doc,
node,
options = { includeAnonymousFunctions: false }
) {
let parentNode = node.parent; let parentNode = node.parent;
while (parentNode !== null) { while (parentNode !== null) {
if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) { if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) {
// For anonymous functions, we use variable declarations, but we only care about variable declarations which are part of function expressions
if (
parentNode.name == nodeTypes.VariableDeclaration &&
!hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions)
) {
parentNode = parentNode.parent;
continue;
}
const funcName = getFunctionName(doc, parentNode); const funcName = getFunctionName(doc, parentNode);
if (funcName) { if (funcName || options.includeAnonymousFunctions) {
return funcName; return {
node: parentNode,
funcName,
};
} }
} }
parentNode = parentNode.parent; parentNode = parentNode.parent;
} }
return ""; return null;
} }
/** /**
@@ -162,9 +184,36 @@ function getEnclosingFunctionName(doc, node) {
* @returns {Object} node - https://lezer.codemirror.net/docs/ref/#common.SyntaxNodeRef * @returns {Object} node - https://lezer.codemirror.net/docs/ref/#common.SyntaxNodeRef
*/ */
function getTreeNodeAtLocation(doc, tree, location) { function getTreeNodeAtLocation(doc, tree, location) {
const line = doc.line(location.line); try {
const pos = line.from + location.column; const line = doc.line(location.line);
return tree.resolve(pos, 1); const pos = line.from + location.column;
return tree.resolve(pos, 1);
} catch (e) {
// if the line is not found in the document doc.line() will throw
console.warn(e.message);
}
return null;
}
/**
* Converts Codemirror position to valid source location. Used only for CM6
*
* @param {Object} doc - The Codemirror document used to retrive the part of content
* @param {Number} pos - Codemirror offset
* @returns
*/
function positionToLocation(doc, pos) {
if (pos == null) {
return {
line: null,
column: null,
};
}
const line = doc.lineAt(pos);
return {
line: line.number,
column: pos - line.from,
};
} }
/** /**
@@ -349,6 +398,28 @@ function getFunctionClass(doc, node) {
); );
} }
/**
* Gets the meta data for member expression nodes
*
* @param {Object} doc - The codemirror document used to retrieve the part of content
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
* @returns
*/
function getMetaBindings(doc, node) {
if (!node || node.name !== nodeTypes.MemberExpression) {
return null;
}
const memExpr = doc.sliceString(node.from, node.to).split(".");
return {
type: "member",
start: positionToLocation(doc, node.from),
end: positionToLocation(doc, node.to),
property: memExpr.at(-1),
parent: getMetaBindings(doc, node.parent),
};
}
/** /**
* Walk the syntax tree of the langauge provided * Walk the syntax tree of the langauge provided
* *
@@ -380,15 +451,34 @@ async function walkTree(view, language, options) {
}); });
} }
/**
* This enables walking a specific part of the syntax tree using the cursor
* provided by the node (which is the parent)
* @param {Object} cursor - https://lezer.codemirror.net/docs/ref/#common.TreeCursor
* @param {Object} options
* {Function} options.enterVisitor - A function that is called when a node is entered
* {Set} options.filterSet - A set of node types which should be visited, all others should be ignored
*/
async function walkCursor(cursor, options) {
await cursor.iterate(node => {
if (options.filterSet?.has(node.name)) {
options.enterVisitor(node);
}
});
}
module.exports = { module.exports = {
getFunctionName, getFunctionName,
getFunctionParameterNames, getFunctionParameterNames,
getFunctionClass, getFunctionClass,
getEnclosingFunctionName, getEnclosingFunction,
getTreeNodeAtLocation, getTreeNodeAtLocation,
getMetaBindings,
nodeTypes, nodeTypes,
nodeTypeSets, nodeTypeSets,
walkTree, walkTree,
getTree, getTree,
clear, clear,
walkCursor,
positionToLocation,
}; };

View File

@@ -26,7 +26,7 @@ function containsLocation(parentLocation, childLocation) {
} }
function getInnerLocations(locations, position) { function getInnerLocations(locations, position) {
// First, find the function which directly contains the specified position (line / column) // First, find the function which directly contains the specified position (line / column)
let parentIndex; let parentIndex;
for (let i = locations.length - 1; i >= 0; i--) { for (let i = locations.length - 1; i >= 0; i--) {
if (containsPosition(locations[i], position)) { if (containsPosition(locations[i], position)) {