package searchfiles import Utilities class SearchBehaviour( val searchForm : SearchForm ) : TableBehaviour( "Search Files" ), Promptable { val results = listOf() var extensions = listOf() var pattern : Pattern = null init { columns = listOf( TableColumn( "Matches", 100 ), TableColumn("File") ) iconName = "search" } // ==== Features ==== val replaceAll = Action( "Replace All", this:>replaceAll ) val refresh = Action( "Refresh", this:>refresh ) .shortcut( Key.F5.noMods() ).icon( "refresh" ) // ==== Methods ==== override meth attached( container : Container ) { super.attached( container ) refresh() } override meth toolbar() = listOf( TextFeature( "Search" ), searchForm.search, TextFeature( "Replace" ), searchForm.replace, replaceAll, refresh ) override meth promptForm() = searchForm meth refresh() { message( "Searching..." ) try { pattern = pattern( searchForm.search.value, searchForm.useRegex.value, searchForm.caseSensitive.value, searchForm.matchWords.value ) extensions = Utilities.extensionsStringToList( searchForm.extensions.value ) newThread( "SearchFiles", this:>refreshThreaded, this:>onFinished ) } catch ( e : Exception ) { message( "Failed : $e" ) } } meth refreshThreaded() : Exception { try { results.clear() if ( searchForm.start.value.isFile() ) { searchFile( searchForm.start.value ) } else if (searchForm.start.value.isFolder()) { searchFolder( searchForm.start.value, searchForm.maxDepth.value ) } } catch (e : Exception) { return e } return null } meth searchFile( file : File ) { // println( "Searching file $file" ) if ( file.length() > SearchFiles.MAX_FILE_SIZE ) { results.add( SearchRow( file, -1, true ) ) return } val contents = file.readText() val matcher = pattern.matcher( contents ) if (matcher.find()) { var matchCount = 0 if ( searchForm.countMatches.value ) { matchCount = 1 for ( i in 1 until searchForm.maxMatchCount.value ) { if (matcher.find()) { matchCount ++ } else { break } } } results.add( SearchRow(file, matchCount, matchCount == searchForm.maxMatchCount.value) ) } } meth fileExtensionMatches( file : File ) : bool { return if ( extensions.isEmpty() ) { return true } else { extensions.contains( file.extension().toLowerCase() ) } } meth searchFolder( folder : File, maxDepth : int ) { println( "Searching folder $folder" ) for (file in folder.files( searchForm.searchHiddenFiles.value )) { if (fileExtensionMatches( file ) ) { searchFile( file ) } } if (maxDepth <= 1) return for (subFolder in folder.folders( searchForm.searchHiddenFolders.value )) { searchFolder( subFolder, maxDepth -1 ) } } meth onFinished( e : Exception ) { if ( e == null ) { if ( results.isEmpty() ) { message( "No files matched" ) } else { message( "${results.size()} files match" ) searchAndReplace().apply{ searchText = searchForm.search.value matchRegex = searchForm.useRegex.value matchCase = searchForm.caseSensitive.value if ( searchForm.replace.value.isBlank() ) { showFindBar() } else { replaceText = searchForm.replace.value showReplaceBar() } } rows.clear() for (row in results) { rows.add( row ) } } } else { message( "Failed : $e" ) } } meth replaceAll() { for (row in results) { val file = row.file val text = file.readText() val matcher = pattern.matcher( text ) val newText = matcher.replaceAll( searchForm.replace.value ) file.writeText( newText ) } } func pattern(search : String, useRegex : bool, caseSensitive : bool, matchWords : bool ): Pattern { var patternString = if (useRegex) { // Ignore CR/LF when the pattern is a Regex. // This lets us spread complex regex over multiple lines to aid readability. // To match new lines, the pattern should include \R (or similar). search.replace("\\R", "") } else { Pattern.quote(search) } if (matchWords) { patternString = "\\b$patternString\\b" } // Should we have the option to disable multi line? var flags = Pattern.MULTILINE if ( ! caseSensitive ) { flags += Pattern.CASE_INSENSITIVE } return Pattern.compile(patternString, flags) } }