@@ -152,22 +152,113 @@ class Locations {
152152 /// Find the [Location] for the given Dart source position.
153153 ///
154154 /// The [line] number is 1-based.
155- Future <Location > locationForDart (DartUri uri, int line, int column) async =>
156- (await locationsForDart (uri.serverPath)).firstWhere (
157- (location) =>
158- location.dartLocation.line == line &&
159- location.dartLocation.column >= column,
160- orElse: () => null );
155+ Future <Location > locationForDart (DartUri uri, int line, int column) async {
156+ var locations = await locationsForDart (uri.serverPath);
157+ return _bestDartLocation (locations, line, column);
158+ }
161159
162160 /// Find the [Location] for the given JS source position.
163161 ///
164162 /// The [line] number is 0-based.
165- Future <Location > locationForJs (String url, int line, int column) async =>
166- (await locationsForUrl (url)).firstWhere (
167- (location) =>
168- location.jsLocation.line == line &&
169- location.jsLocation.column >= column,
170- orElse: () => null );
163+ Future <Location > locationForJs (String url, int line, int column) async {
164+ var locations = await locationsForUrl (url);
165+ return _bestJsLocation (locations, line, column);
166+ }
167+
168+ /// Find closest existing Dart location for the line and column.
169+ ///
170+ /// Dart columns for breakpoints are either exact or start at the
171+ /// beginning of the line - return the first existing location
172+ /// that comes after the given column.
173+ Location _bestDartLocation (
174+ Iterable <Location > locations, int line, int column) {
175+ Location bestLocation;
176+ for (var location in locations) {
177+ if (location.dartLocation.line == line &&
178+ location.dartLocation.column >= column) {
179+ bestLocation ?? = location;
180+ if (location.dartLocation.column < bestLocation.dartLocation.column) {
181+ bestLocation = location;
182+ }
183+ }
184+ }
185+ return bestLocation;
186+ }
187+
188+ /// Find closest existing JavaScript location for the line and column.
189+ ///
190+ /// Some JS locations are not stored in the source maps, so we find the
191+ /// closest existing location, preferring the one coming after the given
192+ /// column, if the current break is at an expression statement, or the
193+ /// one coming before if the current break is at a function call.
194+ ///
195+ /// This is a known problem that other code bases solve using by finding
196+ /// the closest location to the current one:
197+ ///
198+ /// https://github.com/microsoft/vscode-js-debug/blob/536f96bae61a3d87546b61bc7916097904c81429/src/common/sourceUtils.ts#L286
199+ ///
200+ /// Unfortunately, this approach fails for Flutter code too often, as it
201+ /// frequently contains multi-line statements with nested objects.
202+ ///
203+ /// For example:
204+ ///
205+ /// - `t33 = main.doSomething()` in top frame:
206+ /// Current column is at `t33`. Return existing location starting
207+ /// at `main`.
208+ /// - `main.doSomething()` in top frame:
209+ /// Current column is at `main`. Return existing location starting
210+ /// at `main`.
211+ /// - `main.doSomething()` in a frame down the stack:
212+ /// Current column is at `doSomething`. Source map does not have a
213+ /// location stored that starts at `doSomething()`. Return existing
214+ /// location starting at `main`.
215+ Location _bestJsLocation (Iterable <Location > locations, int line, int column) {
216+ Location bestLocationBefore;
217+ Location bestLocationAfter;
218+
219+ var locationsAfter = locations.where ((location) =>
220+ location.jsLocation.line == line &&
221+ location.jsLocation.column >= column);
222+ var locationsBefore = locations.where ((location) =>
223+ location.jsLocation.line == line &&
224+ location.jsLocation.column < column);
225+
226+ for (var location in locationsAfter) {
227+ bestLocationAfter ?? = location;
228+ if (location.jsLocation.column < bestLocationAfter.jsLocation.column) {
229+ bestLocationAfter = location;
230+ }
231+ }
232+ for (var location in locationsBefore) {
233+ bestLocationBefore ?? = location;
234+ if (location.jsLocation.column > bestLocationBefore.jsLocation.column) {
235+ bestLocationBefore = location;
236+ }
237+ }
238+ if (bestLocationAfter == null ) return bestLocationBefore;
239+ if (bestLocationBefore == null ) return bestLocationAfter;
240+
241+ if (bestLocationAfter.jsLocation.line == line &&
242+ bestLocationAfter.jsLocation.column == column) {
243+ // Prefer exact match.
244+ return bestLocationAfter;
245+ }
246+
247+ // Return the closest location after the current if the current location
248+ // is at the beginning of the line (i.e. on expression statement).
249+ // Return the closest location before the current if the current location
250+ // is in the middle of the line (i.e. on function call).
251+ if (locationsBefore.length == 1 &&
252+ locationsBefore.first.jsLocation.column == 0 ) {
253+ // Best guess on whether the the current location is at the beginning of
254+ // the line (i.e. expression statement):
255+ // The only location on the left has column 0, so only spaces are on the
256+ // left.
257+ return bestLocationAfter;
258+ }
259+ // Current column is in the middle of the line (i.e .function call).
260+ return bestLocationBefore;
261+ }
171262
172263 /// Returns the tokenPosTable for the provided Dart script path as defined
173264 /// in:
0 commit comments