diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 09d9622fa0..916719e278 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -39,6 +39,7 @@ - #3233 - Formatting: Fix buffer de-sync when applying formatting edits (fixes #2196, #2820) - #3239 - Buffers: Fix filetype picker not working as expected without an active workspace - #3240 - Formatting: Fix 'Invalid Range Specified' error (fixes #3014) +- #3241 - Extensions: Handle `maxCount` and FS errors in `vscode.workspace.findFiles` (related #3215) ### Performance diff --git a/development_extensions/oni-dev-extension/extension.js b/development_extensions/oni-dev-extension/extension.js index cc571dd7e5..73d2f16125 100644 --- a/development_extensions/oni-dev-extension/extension.js +++ b/development_extensions/oni-dev-extension/extension.js @@ -28,7 +28,18 @@ function activate(context) { vscode.window.showWarningMessage("You clicked developer", []) }), ) - + + cleanup( + vscode.commands.registerCommand("_test.findFiles", (count) => { + vscode.workspace.findFiles( + "*.json", undefined, count + ).then((results) => { + vscode.window.showInformationMessage("success:" + results.length); + // vscode.window.showInformationMessage("success:1"); + }) + }), + ) + cleanup( vscode.commands.registerCommand("developer.oni.hideStatusBar", () => { item.hide(); @@ -37,31 +48,31 @@ function activate(context) { cleanup( vscode.commands.registerCommand("developer.oni.logBufferUpdates", () => { - vscode.window.showInformationMessage("Logging buffer updates!"); - vscode.workspace.onDidChangeTextDocument((e) => { - console.log({ - type: "workspace.onDidChangeTextDocument", - filename: e.document.fileName, - version: e.document.version, - contentChanges: e.contentChanges, - fullText: e.document.getText(), - }); + vscode.window.showInformationMessage("Logging buffer updates!"); + vscode.workspace.onDidChangeTextDocument((e) => { + console.log({ + type: "workspace.onDidChangeTextDocument", + filename: e.document.fileName, + version: e.document.version, + contentChanges: e.contentChanges, + fullText: e.document.getText(), }); + }); }), ) cleanup( vscode.commands.registerCommand("developer.oni.tryOpenDocument", () => { vscode.workspace.openTextDocument(vscode.Uri.file("package.json")) - .then((document) => { - let text = document.getText(); - vscode.window.showInformationMessage("Got text document: " + text); - }, err => { - vscode.window.showErrorMessage("Failed to get text document: " + err.toString()); - }) + .then((document) => { + let text = document.getText(); + vscode.window.showInformationMessage("Got text document: " + text); + }, err => { + vscode.window.showErrorMessage("Failed to get text document: " + err.toString()); + }) }), ) - + cleanup( vscode.commands.registerCommand("developer.oni.showStatusBar", () => { item.show(); @@ -83,7 +94,7 @@ function activate(context) { return [vscode.CompletionItem("HelloWorld"), vscode.CompletionItem("HelloAgain")] }, resolveCompletionItem: (completionItem, token) => { - completionItem.documentation = "RESOLVED documentation: "+ completionItem.label; + completionItem.documentation = "RESOLVED documentation: " + completionItem.label; completionItem.detail = "RESOLVED detail: " + completionItem.label; return completionItem; } @@ -120,10 +131,10 @@ function activate(context) { return new Promise((resolve) => { setTimeout(() => { resolve([ - vscode.CompletionItem("ReasonML0"), - vscode.CompletionItem("OCaml0"), - vscode.CompletionItem("ReasonML2"), - vscode.CompletionItem("OCaml2"), + vscode.CompletionItem("ReasonML0"), + vscode.CompletionItem("OCaml0"), + vscode.CompletionItem("ReasonML2"), + vscode.CompletionItem("OCaml2"), ]) }, 500); }); @@ -135,15 +146,15 @@ function activate(context) { cleanup( vscode.languages.registerCompletionItemProvider("oni-dev", { provideCompletionItems: (document, position, token, context) => { - const items = [ - vscode.CompletionItem("Incomplete" + Date.now().toString()), - ]; - return { - isIncomplete: true, - items, - }; - }, - }), + const items = [ + vscode.CompletionItem("Incomplete" + Date.now().toString()), + ]; + return { + isIncomplete: true, + items, + }; + }, + }), ) const output = vscode.window.createOutputChannel("oni-dev") @@ -153,7 +164,7 @@ function activate(context) { output2.append("Hello output channel!") const collection = vscode.languages.createDiagnosticCollection("test") - + let latestText = "" cleanup( @@ -209,16 +220,16 @@ function activate(context) { cleanup( vscode.commands.registerCommand("developer.oni.showChoiceMessage", () => { vscode.window.showInformationMessage( -//`Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?` -"Hello!" - , "Option 1", "Option 2", "Option 3").then( - (result) => { - vscode.window.showInformationMessage("You picked: " + result) - }, - (err) => { - vscode.window.showInformationMessage("Cancelled: " + err) - }, - ) + //`Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?` + "Hello!" + , "Option 1", "Option 2", "Option 3").then( + (result) => { + vscode.window.showInformationMessage("You picked: " + result) + }, + (err) => { + vscode.window.showInformationMessage("Cancelled: " + err) + }, + ) }), ) @@ -392,7 +403,7 @@ function activate(context) { } // this method is called when your extension is deactivated -function deactivate() {} +function deactivate() { } module.exports = { activate, diff --git a/integration_test/ExtHostWorkspaceSearchTest.re b/integration_test/ExtHostWorkspaceSearchTest.re new file mode 100644 index 0000000000..f1514629d3 --- /dev/null +++ b/integration_test/ExtHostWorkspaceSearchTest.re @@ -0,0 +1,69 @@ +open Oni_Model; +open Oni_IntegrationTestLib; + +// This test validates: +// - The 'oni-dev' extension gets activated +// - When typing in an 'oni-dev' buffer, we get some completion results +runTest(~name="ExtHostWorkspaceSearchTest", ({dispatch, wait, _}) => { + wait(~timeout=30.0, ~name="Exthost is initialized", (state: State.t) => + Feature_Exthost.isInitialized(state.exthost) + ); + + // Wait until the extension is activated + // Give some time for the exthost to start + wait( + ~timeout=30.0, + ~name="Validate the 'oni-dev' extension gets activated", + (state: State.t) => + List.exists( + id => id == "oni-dev-extension", + state.extensions |> Feature_Extensions.activatedIds, + ) + ); + + // Search for 1 match + dispatch( + Actions.Extensions( + Feature_Extensions.Msg.command( + ~command="_test.findFiles", + ~arguments=[`Int(1)], + ), + ), + ); + + // Wait for a notification + wait( + ~timeout=30., + ~name="Got success notification for 1 result", + (state: State.t) => { + let notifications = Feature_Notification.all(state.notifications); + notifications + |> List.exists((notification: Feature_Notification.notification) => { + notification.message == "success:1" + }); + }, + ); + + // Search for multiple matches + dispatch( + Actions.Extensions( + Feature_Extensions.Msg.command( + ~command="_test.findFiles", + ~arguments=[`Int(3)], + ), + ), + ); + + // Wait for a notification + wait( + ~timeout=60., + ~name="Got success notification for 5 results", + (state: State.t) => { + let notifications = Feature_Notification.all(state.notifications); + notifications + |> List.exists((notification: Feature_Notification.notification) => { + notification.message == "success:3" + }); + }, + ); +}); diff --git a/integration_test/dune b/integration_test/dune index e8e8868bab..386d3e0d77 100644 --- a/integration_test/dune +++ b/integration_test/dune @@ -8,24 +8,24 @@ EditorUtf8Test ExCommandKeybindingTest ExCommandKeybindingWithArgsTest ExCommandKeybindingNormTest ExtConfigurationChangedTest ExtHostBufferOpenTest ExtHostBufferUpdatesTest ExtHostCompletionTest - ExtHostDefinitionTest FileWatcherTest KeybindingsInvalidJsonTest - KeySequenceJJTest InputIgnoreTest InputContextKeysTest - InputRemapMotionTest LanguageCssTest LanguageTypeScriptTest - LineEndingsLFTest LineEndingsCRLFTest QuickOpenEventuallyCompletesTest - SearchShowClearHighlightsTest SyntaxServer SyntaxServerCloseTest - SyntaxServerMessageExceptionTest SyntaxServerParentPidTest - SyntaxServerParentPidCorrectTest SyntaxServerReadExceptionTest - Regression1671Test Regression2349EnewTest Regression2988SwitchEditorTest - Regression3084CommandLoopTest RegressionCommandLineNoCompletionTest - RegressionFontFallbackTest RegressionFileModifiedIndicationTest - RegressionNonExistentDirectory RegressionVspEmptyInitialBufferTest - RegressionVspEmptyExistingBufferTest SCMGitTest SyntaxHighlightPhpTest - SyntaxHighlightTextMateTest SyntaxHighlightTreesitterTest - AddRemoveSplitTest TerminalSetPidTitleTest TerminalConfigurationTest - TypingBatchedTest TypingUnbatchedTest VimIncsearchScrollTest - VimExperimentalVimlTest VimRemapTimeoutTest VimSimpleRemapTest - VimlRemapCmdlineTest ClipboardChangeTest VimScriptLocalFunctionTest - ZenModeSingleFileModeTest ZenModeSplitTest) + ExtHostDefinitionTest ExtHostWorkspaceSearchTest FileWatcherTest + KeybindingsInvalidJsonTest KeySequenceJJTest InputIgnoreTest + InputContextKeysTest InputRemapMotionTest LanguageCssTest + LanguageTypeScriptTest LineEndingsLFTest LineEndingsCRLFTest + QuickOpenEventuallyCompletesTest SearchShowClearHighlightsTest + SyntaxServer SyntaxServerCloseTest SyntaxServerMessageExceptionTest + SyntaxServerParentPidTest SyntaxServerParentPidCorrectTest + SyntaxServerReadExceptionTest Regression1671Test Regression2349EnewTest + Regression2988SwitchEditorTest Regression3084CommandLoopTest + RegressionCommandLineNoCompletionTest RegressionFontFallbackTest + RegressionFileModifiedIndicationTest RegressionNonExistentDirectory + RegressionVspEmptyInitialBufferTest RegressionVspEmptyExistingBufferTest + SCMGitTest SyntaxHighlightPhpTest SyntaxHighlightTextMateTest + SyntaxHighlightTreesitterTest AddRemoveSplitTest TerminalSetPidTitleTest + TerminalConfigurationTest TypingBatchedTest TypingUnbatchedTest + VimIncsearchScrollTest VimExperimentalVimlTest VimRemapTimeoutTest + VimSimpleRemapTest VimlRemapCmdlineTest ClipboardChangeTest + VimScriptLocalFunctionTest ZenModeSingleFileModeTest ZenModeSplitTest) (libraries Oni_CLI Oni_IntegrationTestLib reason-native-crash-utils.asan)) (install @@ -43,13 +43,13 @@ ExCommandKeybindingNormTest.exe ExCommandKeybindingWithArgsTest.exe ExtConfigurationChangedTest.exe ExtHostBufferOpenTest.exe ExtHostBufferUpdatesTest.exe ExtHostCompletionTest.exe - ExtHostDefinitionTest.exe FileWatcherTest.exe - KeybindingsInvalidJsonTest.exe KeySequenceJJTest.exe InputIgnoreTest.exe - InputContextKeysTest.exe InputRemapMotionTest.exe LanguageCssTest.exe - LanguageTypeScriptTest.exe LineEndingsCRLFTest.exe LineEndingsLFTest.exe - QuickOpenEventuallyCompletesTest.exe Regression1671Test.exe - Regression2349EnewTest.exe Regression2988SwitchEditorTest.exe - Regression3084CommandLoopTest.exe + ExtHostDefinitionTest.exe ExtHostWorkspaceSearchTest.exe + FileWatcherTest.exe KeybindingsInvalidJsonTest.exe KeySequenceJJTest.exe + InputIgnoreTest.exe InputContextKeysTest.exe InputRemapMotionTest.exe + LanguageCssTest.exe LanguageTypeScriptTest.exe LineEndingsCRLFTest.exe + LineEndingsLFTest.exe QuickOpenEventuallyCompletesTest.exe + Regression1671Test.exe Regression2349EnewTest.exe + Regression2988SwitchEditorTest.exe Regression3084CommandLoopTest.exe RegressionCommandLineNoCompletionTest.exe RegressionFileModifiedIndicationTest.exe RegressionFontFallbackTest.exe RegressionVspEmptyInitialBufferTest.exe RegressionNonExistentDirectory.exe diff --git a/src/Service/OS/Service_OS.re b/src/Service/OS/Service_OS.re index 1290ce6961..900b6747a0 100644 --- a/src/Service/OS/Service_OS.re +++ b/src/Service/OS/Service_OS.re @@ -60,48 +60,67 @@ module Api = { let rec fold = ( + ~shouldContinue: 'a => bool, ~includeFiles, ~excludeDirectory, ~initial, accumulateFn: ('a, string) => 'a, rootPath, - ) => { - readdir(rootPath) - |> LwtEx.flatMap(entries => { - entries - |> List.fold_left( - (accPromise, {kind, name}: Luv.File.Dirent.t) => { - let fullPath = Rench.Path.join(rootPath, name); - if (kind == `FILE && includeFiles(fullPath)) { - let promise: Lwt.t('a) = - accPromise - |> LwtEx.flatMap(acc => { - Lwt.return(accumulateFn(acc, fullPath)) - }); - promise; - } else if (kind == `DIR && !excludeDirectory(fullPath)) { - let promise: Lwt.t('a) = - accPromise - |> LwtEx.flatMap(acc => { - fold( - ~includeFiles, - ~excludeDirectory, - ~initial=acc, - accumulateFn, - fullPath, - ) - }); - promise; - } else { - accPromise; - }; - }, - Lwt.return(initial), + ) => + if (!shouldContinue(initial)) { + Lwt.return(initial); + } else { + Lwt.catch( + () => { + readdir(rootPath) + |> LwtEx.flatMap(entries => { + entries + |> List.fold_left( + (accPromise, {kind, name}: Luv.File.Dirent.t) => { + let fullPath = Rench.Path.join(rootPath, name); + if (kind == `FILE && includeFiles(fullPath)) { + let promise: Lwt.t('a) = + accPromise + |> LwtEx.flatMap(acc => { + Lwt.return(accumulateFn(acc, fullPath)) + }); + promise; + } else if (kind == `DIR && !excludeDirectory(fullPath)) { + let promise: Lwt.t('a) = + accPromise + |> LwtEx.flatMap(acc => { + fold( + ~shouldContinue, + ~includeFiles, + ~excludeDirectory, + ~initial=acc, + accumulateFn, + fullPath, + ) + }); + promise; + } else { + accPromise; + }; + }, + Lwt.return(initial), + ) + }) + }, + exn => { + Log.warnf(m => + m( + "Error while running Service_OS.fold on %s: %s", + rootPath, + Printexc.to_string(exn), ) - }); - }; + ); + Lwt.return(initial); + }, + ); + }; - let glob = (~includeFiles=?, ~excludeDirectories=?, path) => { + let glob = (~maxCount=?, ~includeFiles=?, ~excludeDirectories=?, path) => { let includeFilesFn = includeFiles |> Option.map(filesGlobStr => { @@ -120,13 +139,26 @@ module Api = { }) |> Option.value(~default=_ => false); + let shouldContinue = + switch (maxCount) { + | None => (_ => true) + | Some(max) => (list => ListEx.boundedLength(~max, list) < max) + }; + fold( + ~shouldContinue, ~includeFiles=includeFilesFn, ~excludeDirectory=excludeDirectoryFn, ~initial=[], (acc, curr) => [curr, ...acc], path, - ); + ) + |> Lwt.map(items => { + switch (maxCount) { + | None => items + | Some(count) => items |> ListEx.firstk(count) + } + }); }; let readFile = (~chunkSize=4096, path) => { diff --git a/src/Service/OS/Service_OS.rei b/src/Service/OS/Service_OS.rei index 7f4a1df9ce..171d8044b6 100644 --- a/src/Service/OS/Service_OS.rei +++ b/src/Service/OS/Service_OS.rei @@ -1,18 +1,13 @@ open Oni_Core; module Api: { - let fold: + let glob: ( - ~includeFiles: string => bool, - ~excludeDirectory: string => bool, - ~initial: 'a, - ('a, string) => 'a, + ~maxCount: int=?, + ~includeFiles: string=?, + ~excludeDirectories: string=?, string ) => - Lwt.t('a); - - let glob: - (~includeFiles: string=?, ~excludeDirectories: string=?, string) => Lwt.t(list(string)); let rmdir: (~recursive: bool=?, string) => Lwt.t(unit); diff --git a/src/Store/ExtensionClient.re b/src/Store/ExtensionClient.re index 821e84bff1..5fd5fa1ab1 100644 --- a/src/Store/ExtensionClient.re +++ b/src/Store/ExtensionClient.re @@ -204,8 +204,11 @@ let create = : Lwt.return(Reply.error("Unable to open URI")) | Window(GetWindowVisibility) => Lwt.return(Reply.okJson(`Bool(true))) - | Workspace(StartFileSearch({includePattern, excludePattern, _})) => + | Workspace( + StartFileSearch({includePattern, excludePattern, maxResults}), + ) => Service_OS.Api.glob( + ~maxCount=?maxResults, ~includeFiles=?includePattern, ~excludeDirectories=?excludePattern, // TODO: Pull from store