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,
} from "../../selectors/index";
import { features } from "../../utils/prefs";
import { getEditor } from "../../utils/editor/index";
import { validateSelectedFrame } from "../../utils/context";
/**
@@ -24,8 +25,8 @@ export function generateInlinePreview(selectedFrame) {
if (getSelectedFrameInlinePreviews(getState())) {
return null;
}
const scope = getSelectedScope(getState());
const scope = getSelectedScope(getState());
if (!scope || !scope.bindings) {
return null;
}
@@ -52,6 +53,8 @@ export function generateInlinePreview(selectedFrame) {
previews[line].push(preview);
}
validateSelectedFrame(getState(), selectedFrame);
return dispatch({
type: "ADD_INLINE_PREVIEW",
selectedFrame,
@@ -63,7 +66,7 @@ export function generateInlinePreview(selectedFrame) {
* Creates all the previews
*
* @param {Object} selectedFrame
* @param {Object} scope - Scopes from the platform
* @param {Object} scope - Scope from the platform
* @param {Object} thunkArgs
* @returns
*/
@@ -81,46 +84,79 @@ async function getPreviews(selectedFrame, scope, thunkArgs) {
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 = [];
let level = 0;
while (scope && scope.bindings) {
// All the bindings from the platform environment
const bindings = getScopeBindings(scope);
if (features.codemirrorNext) {
// Get all the bindings for all scopes up until and including the first function scope.
let allBindings = {};
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
const allPreviewBindingsComplete = Object.keys(bindings).map(async name => {
// Get previews for this binding
validateSelectedFrame(getState(), selectedFrame);
for (const name in references) {
const previews = await generatePreviewsForBinding(
originalAstScopes[level]?.bindings[name],
references[name],
selectedLocation.line,
name,
bindings[name].value,
allBindings[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;
} else {
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;
}

View File

@@ -1047,7 +1047,8 @@ class Editor extends EventEmitter {
// investigate further Bug 1890895.
event.target.ownerGlobal.setTimeout(() => {
const view = editor.viewState;
const cursorPos = this.#posToLineColumn(
const cursorPos = lezerUtils.positionToLocation(
view.state.doc,
view.state.selection.main.head
);
handler(event, view, cursorPos.line, cursorPos.column);
@@ -1868,12 +1869,16 @@ class Editor extends EventEmitter {
const {
codemirrorLanguage: { syntaxTree },
} = this.#CodeMirror6;
const lineObject = cm.state.doc.line(line);
const pos = lineObject.from + column;
const token = syntaxTree(cm.state).resolve(pos, 1);
const token = lezerUtils.getTreeNodeAtLocation(
cm.state.doc,
syntaxTree(cm.state),
{ line, column }
);
if (!token) {
return null;
}
return {
startColumn: column,
endColumn: token.to - token.from,
@@ -2150,7 +2155,10 @@ class Editor extends EventEmitter {
handle: {
markedSpans: markedSpans
? markedSpans.map(span => {
const { column } = this.#posToLineColumn(cm.posAtDOM(span));
const { column } = lezerUtils.positionToLocation(
cm.state.doc,
cm.posAtDOM(span)
);
return {
marker: { className: span.className },
from: column,
@@ -2198,8 +2206,8 @@ class Editor extends EventEmitter {
name,
klass: lezerUtils.getFunctionClass(cm.state.doc, syntaxNode),
location: {
start: this.#posToLineColumn(node.from),
end: this.#posToLineColumn(node.to),
start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: lezerUtils.positionToLocation(cm.state.doc, node.to),
},
parameterNames: lezerUtils.getFunctionParameterNames(
cm.state.doc,
@@ -2236,8 +2244,8 @@ class Editor extends EventEmitter {
classVarDefNode.to
),
location: {
start: this.#posToLineColumn(node.from),
end: this.#posToLineColumn(node.to),
start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: lezerUtils.positionToLocation(cm.state.doc, node.to),
},
});
},
@@ -2290,8 +2298,14 @@ class Editor extends EventEmitter {
doc = cm.state.toText(sourceContent);
tree = lezerUtils.getTree(javascriptLanguage, sourceId, sourceContent);
}
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,
expression: cm.state.doc.sliceString(node.from, node.to),
location: {
start: this.#posToLineColumn(node.from),
end: this.#posToLineColumn(node.to),
start: lezerUtils.positionToLocation(cm.state.doc, node.from),
end: lezerUtils.positionToLocation(cm.state.doc, node.to),
},
from: node.from,
to: node.to,
@@ -2435,6 +2449,73 @@ class Editor extends EventEmitter {
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
* the 'value' argument.
@@ -3396,27 +3477,6 @@ class Editor extends EventEmitter {
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
* @param {Number} line - The line in the source
@@ -3617,7 +3677,10 @@ class Editor extends EventEmitter {
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
return {
text: cursor.match[0],

View File

@@ -71,6 +71,11 @@ const nodeTypeSets = {
numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]),
memberExpression: new Set([nodeTypes.MemberExpression]),
classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]),
bindingReferences: new Set([
nodeTypes.VariableDefinition,
nodeTypes.VariableName,
]),
expressionProperty: new Set([nodeTypes.PropertyName]),
};
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} 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
*/
function getEnclosingFunctionName(doc, node) {
function getEnclosingFunction(
doc,
node,
options = { includeAnonymousFunctions: false }
) {
let parentNode = node.parent;
while (parentNode !== null) {
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);
if (funcName) {
return funcName;
if (funcName || options.includeAnonymousFunctions) {
return {
node: parentNode,
funcName,
};
}
}
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
*/
function getTreeNodeAtLocation(doc, tree, location) {
const line = doc.line(location.line);
const pos = line.from + location.column;
return tree.resolve(pos, 1);
try {
const line = doc.line(location.line);
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
*
@@ -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 = {
getFunctionName,
getFunctionParameterNames,
getFunctionClass,
getEnclosingFunctionName,
getEnclosingFunction,
getTreeNodeAtLocation,
getMetaBindings,
nodeTypes,
nodeTypeSets,
walkTree,
getTree,
clear,
walkCursor,
positionToLocation,
};

View File

@@ -26,7 +26,7 @@ function containsLocation(parentLocation, childLocation) {
}
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;
for (let i = locations.length - 1; i >= 0; i--) {
if (containsPosition(locations[i], position)) {